stagehand 4.3.0a0.dev1620__tar.gz → 4.3.0a0.dev1626__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/PKG-INFO +1 -1
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/pyproject.toml +1 -1
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/service-worker.js +191 -44
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/input_types.py +22 -4
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/models.py +45 -7
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/protocol_version.py +1 -1
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_validation.py +61 -2
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browser.py +9 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/cdp_client.py +98 -10
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/client_models.py +5 -2
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/client_types.py +2 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/page.py +45 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/rpc_client.py +8 -4
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/README.md +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/__init__.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/blank.html +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/content-script.js +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/manifest.json +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/wake-service-worker.html +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/wake-service-worker.js +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/__init__.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_sdk_identity.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browser_clipboard.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browser_context.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_connection.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_rpc.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_services.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_session.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_transport.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/extension_assets.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/file_upload.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/locator.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/py.typed +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/response.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/stagehand.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/timeouts.py +0 -0
- {stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/webmcp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stagehand
|
|
3
|
-
Version: 4.3.0a0.
|
|
3
|
+
Version: 4.3.0a0.dev1626
|
|
4
4
|
Summary: Browserbase's SDK for building browser agents
|
|
5
5
|
Keywords: ai,agent,ai-agent,browser,browser-agent,browser-automation,computer-use,headless,playwright,cdp,llm,automation,web-scraping,testing
|
|
6
6
|
Author: Browserbase
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/service-worker.js
RENAMED
|
@@ -7363,7 +7363,7 @@ function isRecord$1(value) {
|
|
|
7363
7363
|
}
|
|
7364
7364
|
var package_default$1 = {
|
|
7365
7365
|
name: "@browserbasehq/stagehand-protocol",
|
|
7366
|
-
version: "3.0.0-alpha-
|
|
7366
|
+
version: "3.0.0-alpha-901eb276429702cddd4bfe3a3656946f3ebc166a",
|
|
7367
7367
|
"private": true,
|
|
7368
7368
|
type: "module",
|
|
7369
7369
|
exports: {
|
|
@@ -8553,6 +8553,13 @@ var PageWaitForLoadStateParamsSchema = PageIdParamsSchema.extend({
|
|
|
8553
8553
|
timeout: number$1().int().nonnegative().optional()
|
|
8554
8554
|
}).meta({ id: "PageWaitForLoadStateParams" });
|
|
8555
8555
|
var PageWaitForTimeoutParamsSchema = PageIdParamsSchema.extend({ ms: number$1().int().nonnegative() }).meta({ id: "PageWaitForTimeoutParams" });
|
|
8556
|
+
var PageWaitForFunctionParamsSchema = PageIdParamsSchema.extend({
|
|
8557
|
+
expression: string(),
|
|
8558
|
+
options: strictObject({
|
|
8559
|
+
polling: union([literal("raf"), number$1().positive().max(2147483647)]).optional(),
|
|
8560
|
+
timeout: number$1().int().nonnegative().optional()
|
|
8561
|
+
}).meta({ id: "PageWaitForFunctionOptions" }).optional()
|
|
8562
|
+
}).meta({ id: "PageWaitForFunctionParams" });
|
|
8556
8563
|
var PageWaitForSelectorParamsSchema = PageIdParamsSchema.extend({
|
|
8557
8564
|
selector: string().min(1),
|
|
8558
8565
|
options: strictObject({
|
|
@@ -8626,6 +8633,7 @@ var PageEvaluateResultSchema = strictObject({ value: json$1() }).meta({ id: "Pag
|
|
|
8626
8633
|
var PageScreenshotResultSchema = strictObject({ data: base64().meta({ format: "byte" }) }).meta({ id: "PageScreenshotResult" });
|
|
8627
8634
|
var PagePDFResultSchema = strictObject({ data: base64().meta({ format: "byte" }) }).meta({ id: "PagePDFResult" });
|
|
8628
8635
|
var PageWaitForSelectorResultSchema = strictObject({ matched: boolean() }).meta({ id: "PageWaitForSelectorResult" });
|
|
8636
|
+
var PageWaitForFunctionResultSchema = strictObject({ value: json$1() }).meta({ id: "PageWaitForFunctionResult" });
|
|
8629
8637
|
var LocatorClickResultSchema = strictObject({ clicked: literal(true) }).meta({ id: "LocatorClickResult" });
|
|
8630
8638
|
var LocatorFillResultSchema = strictObject({ filled: literal(true) }).meta({ id: "LocatorFillResult" });
|
|
8631
8639
|
var LocatorHoverResultSchema = strictObject({ hovered: literal(true) }).meta({ id: "LocatorHoverResult" });
|
|
@@ -8940,6 +8948,12 @@ var StagehandMethods = {
|
|
|
8940
8948
|
params: PageWaitForSelectorParamsSchema,
|
|
8941
8949
|
result: PageWaitForSelectorResultSchema
|
|
8942
8950
|
},
|
|
8951
|
+
pageWaitForFunction: {
|
|
8952
|
+
name: "page.wait_for_function",
|
|
8953
|
+
params: PageWaitForFunctionParamsSchema,
|
|
8954
|
+
result: PageWaitForFunctionResultSchema,
|
|
8955
|
+
resultWire: { opaqueKeys: ["value"] }
|
|
8956
|
+
},
|
|
8943
8957
|
pageWebMCPTools: {
|
|
8944
8958
|
name: "page.webmcp_tools",
|
|
8945
8959
|
params: PageWebMCPToolsParamsSchema,
|
|
@@ -12001,9 +12015,18 @@ var Locator$1 = class Locator$1 {
|
|
|
12001
12015
|
};
|
|
12002
12016
|
//#endregion
|
|
12003
12017
|
//#region ../sdk-ts/src/pageScripts.ts
|
|
12004
|
-
function normalizeEvaluationExpression(expression, arg) {
|
|
12018
|
+
function normalizeEvaluationExpression(expression, arg, caller = "page.evaluate") {
|
|
12005
12019
|
if (typeof expression === "string") return expression;
|
|
12006
|
-
return `(${expression.toString()})(${serializeArgument(arg,
|
|
12020
|
+
return `(${expression.toString()})(${serializeArgument(arg, caller)})`;
|
|
12021
|
+
}
|
|
12022
|
+
function normalizeFunctionWaitExpression(expression, arg, caller = "page.waitForFunction") {
|
|
12023
|
+
if (typeof expression === "function") return `() => (${expression.toString()})(${parsedArgument(arg, caller)})`;
|
|
12024
|
+
if (arg === void 0) return expression;
|
|
12025
|
+
return `((value) => typeof value === "function" ? () => value(${parsedArgument(arg, caller)}) : value)((${expression}))`;
|
|
12026
|
+
}
|
|
12027
|
+
function parsedArgument(arg, caller) {
|
|
12028
|
+
if (arg === void 0) return "undefined";
|
|
12029
|
+
return `JSON.parse(${JSON.stringify(serializeArgument(arg, caller))})`;
|
|
12007
12030
|
}
|
|
12008
12031
|
async function normalizeInitScriptSource$1(script, arg, caller = "page.addInitScript") {
|
|
12009
12032
|
if (typeof script === "function") return `(${script.toString()})(${serializeArgument(arg, caller)})`;
|
|
@@ -12026,9 +12049,8 @@ async function normalizeInitScriptSource$1(script, arg, caller = "page.addInitSc
|
|
|
12026
12049
|
}
|
|
12027
12050
|
function serializeArgument(arg, caller) {
|
|
12028
12051
|
if (arg === void 0) return "undefined";
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
const serialized = JSON.stringify(parsed.data);
|
|
12052
|
+
if (!json$1().safeParse(arg).success) throw new TypeError(`${caller}: 'arg' must be JSON-serializable.`);
|
|
12053
|
+
const serialized = JSON.stringify(arg);
|
|
12032
12054
|
if (serialized === void 0) throw new TypeError(`${caller}: 'arg' must be JSON-serializable.`);
|
|
12033
12055
|
return serialized;
|
|
12034
12056
|
}
|
|
@@ -12362,6 +12384,14 @@ var Page$1 = class {
|
|
|
12362
12384
|
...options ? { options } : {}
|
|
12363
12385
|
})).matched;
|
|
12364
12386
|
}
|
|
12387
|
+
/** Poll in the page's main world until the expression returns a truthy JSON value. */
|
|
12388
|
+
async waitForFunction(expression, arg, options) {
|
|
12389
|
+
return (await this.rpcClient.send(StagehandMethods.pageWaitForFunction, {
|
|
12390
|
+
pageId: this.pageId,
|
|
12391
|
+
expression: normalizeFunctionWaitExpression(expression, arg),
|
|
12392
|
+
...options ? { options } : {}
|
|
12393
|
+
})).value;
|
|
12394
|
+
}
|
|
12365
12395
|
async screenshot(options) {
|
|
12366
12396
|
const { path, mask, ...screenshotOptions } = options ?? {};
|
|
12367
12397
|
return await decodeCaptureResult((await this.rpcClient.send(StagehandMethods.pageScreenshot, {
|
|
@@ -12527,8 +12557,20 @@ strictObject({
|
|
|
12527
12557
|
acceptDownloads: boolean().optional(),
|
|
12528
12558
|
keepAlive: boolean().optional()
|
|
12529
12559
|
}).meta({ id: "LocalBrowserLaunchOptions" });
|
|
12560
|
+
var CDPHeadersSchema = preprocess((value) => {
|
|
12561
|
+
try {
|
|
12562
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
|
|
12563
|
+
const entries = Object.entries(value);
|
|
12564
|
+
if (!entries.every(([name, content]) => name.length > 0 && !/[^!#$%&'*+\-.^_`|~0-9A-Za-z]/.test(name) && typeof content === "string" && !/[^\t\x20-\x7e]/.test(content) && content === content.replace(/^[ \t]+|[ \t]+$/g, ""))) return null;
|
|
12565
|
+
return Object.fromEntries(entries);
|
|
12566
|
+
} catch {
|
|
12567
|
+
return null;
|
|
12568
|
+
}
|
|
12569
|
+
}, record(string(), string(), { error: "headers must map valid HTTP header names to ASCII string values without leading or trailing spaces or tabs" }));
|
|
12530
12570
|
strictObject({
|
|
12531
12571
|
cdpUrl: string().min(1),
|
|
12572
|
+
/** Headers for client-side CDP discovery and WebSocket connections. */
|
|
12573
|
+
headers: CDPHeadersSchema.optional(),
|
|
12532
12574
|
extensionId: string().optional(),
|
|
12533
12575
|
/**
|
|
12534
12576
|
* Browser-level CDP WebSocket URL dialed by the extension inside Chrome.
|
|
@@ -13005,6 +13047,10 @@ function createPageController(runtime) {
|
|
|
13005
13047
|
logger.debug("page.wait_for_selector", {});
|
|
13006
13048
|
return runtime.pageWaitForSelector(params);
|
|
13007
13049
|
}
|
|
13050
|
+
async function waitForFunction(params, { logger }) {
|
|
13051
|
+
logger.debug("page.wait_for_function", {});
|
|
13052
|
+
return runtime.pageWaitForFunction(params);
|
|
13053
|
+
}
|
|
13008
13054
|
async function screenshot(params, { logger }) {
|
|
13009
13055
|
logger.debug("page.screenshot", {});
|
|
13010
13056
|
return runtime.pageScreenshot(params);
|
|
@@ -13071,6 +13117,7 @@ function createPageController(runtime) {
|
|
|
13071
13117
|
waitForLoadState,
|
|
13072
13118
|
waitForTimeout,
|
|
13073
13119
|
waitForSelector,
|
|
13120
|
+
waitForFunction,
|
|
13074
13121
|
screenshot,
|
|
13075
13122
|
pdf,
|
|
13076
13123
|
snapshot,
|
|
@@ -16943,6 +16990,38 @@ async function act$1(params) {
|
|
|
16943
16990
|
};
|
|
16944
16991
|
}
|
|
16945
16992
|
//#endregion
|
|
16993
|
+
//#region understudy/browserWait.ts
|
|
16994
|
+
/** Browser exceptions are not transient CDP context-loss errors. */
|
|
16995
|
+
var BrowserWaitError = class extends Error {};
|
|
16996
|
+
/** Own a browser-side promise and dispose its resources, even after late installation. */
|
|
16997
|
+
async function browserWait(session, progress, install, phase) {
|
|
16998
|
+
const dispose = (objectId) => Promise.all([progress.cleanup(() => session.send("Runtime.callFunctionOn", {
|
|
16999
|
+
objectId,
|
|
17000
|
+
functionDeclaration: "function() { this.dispose(); }",
|
|
17001
|
+
returnByValue: true
|
|
17002
|
+
})), progress.cleanup(() => session.send("Runtime.releaseObject", { objectId }))]);
|
|
17003
|
+
const response = await install(async (late) => {
|
|
17004
|
+
if (late.result.objectId) await dispose(late.result.objectId);
|
|
17005
|
+
});
|
|
17006
|
+
const objectId = response.result.objectId;
|
|
17007
|
+
let completed = false;
|
|
17008
|
+
try {
|
|
17009
|
+
if (response.exceptionDetails || !objectId) throw new BrowserWaitError(response.exceptionDetails?.exception?.description ?? response.exceptionDetails?.text ?? "Browser wait handle was not returned");
|
|
17010
|
+
const result = await progress.run(phase, () => session.send("Runtime.callFunctionOn", {
|
|
17011
|
+
objectId,
|
|
17012
|
+
functionDeclaration: "function() { return this.promise; }",
|
|
17013
|
+
awaitPromise: true,
|
|
17014
|
+
returnByValue: true
|
|
17015
|
+
}));
|
|
17016
|
+
if (result.exceptionDetails) throw new BrowserWaitError(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "Browser wait failed");
|
|
17017
|
+
completed = true;
|
|
17018
|
+
return result.result.value;
|
|
17019
|
+
} finally {
|
|
17020
|
+
if (objectId) if (completed) progress.cleanup(() => session.send("Runtime.releaseObject", { objectId }));
|
|
17021
|
+
else await dispose(objectId);
|
|
17022
|
+
}
|
|
17023
|
+
}
|
|
17024
|
+
//#endregion
|
|
16946
17025
|
//#region understudy/frame.ts
|
|
16947
17026
|
function base64ToBytes$1(base64) {
|
|
16948
17027
|
const binary = globalThis.atob(base64);
|
|
@@ -17068,46 +17147,25 @@ var Frame = class {
|
|
|
17068
17147
|
}
|
|
17069
17148
|
/** Own a disposable browser wait, including a handle delivered after expiry. */
|
|
17070
17149
|
async waitInLocatorWorld(buildExpression, progress) {
|
|
17071
|
-
const dispose = (objectId) => Promise.all([progress.cleanup(() => this.session.send("Runtime.callFunctionOn", {
|
|
17072
|
-
objectId,
|
|
17073
|
-
functionDeclaration: "function() { this.dispose(); }",
|
|
17074
|
-
returnByValue: true
|
|
17075
|
-
})), progress.cleanup(() => this.session.send("Runtime.releaseObject", { objectId }))]);
|
|
17076
17150
|
for (let attempt = 0;; attempt++) {
|
|
17077
17151
|
progress.throwIfStopped();
|
|
17078
|
-
|
|
17079
|
-
progress,
|
|
17080
|
-
returnByValue: false,
|
|
17081
|
-
awaitPromise: false,
|
|
17082
|
-
onLateResult: async (late) => {
|
|
17083
|
-
if (late.result.objectId) await dispose(late.result.objectId);
|
|
17084
|
-
}
|
|
17085
|
-
});
|
|
17086
|
-
const objectId = response.result.objectId;
|
|
17087
|
-
let completed = false;
|
|
17152
|
+
let contextId;
|
|
17088
17153
|
try {
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
}
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
}
|
|
17105
|
-
if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "Selector wait failed");
|
|
17106
|
-
completed = true;
|
|
17107
|
-
return result.result.value;
|
|
17108
|
-
} finally {
|
|
17109
|
-
if (objectId) if (completed) progress.cleanup(() => this.session.send("Runtime.releaseObject", { objectId }));
|
|
17110
|
-
else await dispose(objectId);
|
|
17154
|
+
return await browserWait(this.session, progress, async (onLateResult) => {
|
|
17155
|
+
const response = await this.evaluateLocatorExpression(buildExpression, {
|
|
17156
|
+
progress,
|
|
17157
|
+
returnByValue: false,
|
|
17158
|
+
awaitPromise: false,
|
|
17159
|
+
onLateResult
|
|
17160
|
+
});
|
|
17161
|
+
contextId = response.contextId;
|
|
17162
|
+
return response;
|
|
17163
|
+
}, "waiting for selector");
|
|
17164
|
+
} catch (error) {
|
|
17165
|
+
progress.throwIfStopped();
|
|
17166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17167
|
+
if (attempt !== 0 || contextId === void 0 || error instanceof BrowserWaitError || !message.includes("Cannot find context with specified id")) throw error;
|
|
17168
|
+
executionContexts.unregisterLocatorContext(this.session, contextId);
|
|
17111
17169
|
}
|
|
17112
17170
|
}
|
|
17113
17171
|
}
|
|
@@ -52076,6 +52134,7 @@ var RPCRouter = class {
|
|
|
52076
52134
|
case "page.wait_for_load_state": return this.pageController.waitForLoadState(parseParams(StagehandMethods.pageWaitForLoadState, request.params), context);
|
|
52077
52135
|
case "page.wait_for_timeout": return this.pageController.waitForTimeout(parseParams(StagehandMethods.pageWaitForTimeout, request.params), context);
|
|
52078
52136
|
case "page.wait_for_selector": return this.pageController.waitForSelector(parseParams(StagehandMethods.pageWaitForSelector, request.params), context);
|
|
52137
|
+
case "page.wait_for_function": return this.pageController.waitForFunction(parseParams(StagehandMethods.pageWaitForFunction, request.params), context);
|
|
52079
52138
|
case "page.screenshot": return this.pageController.screenshot(parseParams(StagehandMethods.pageScreenshot, request.params), context);
|
|
52080
52139
|
case "page.pdf": return this.pageController.pdf(parseParams(StagehandMethods.pagePDF, request.params), context);
|
|
52081
52140
|
case "page.snapshot": return this.pageController.snapshot(parseParams(StagehandMethods.pageSnapshot, request.params), context);
|
|
@@ -54471,7 +54530,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
|
|
|
54471
54530
|
};
|
|
54472
54531
|
var package_default = {
|
|
54473
54532
|
name: "@browserbasehq/stagehand-extension",
|
|
54474
|
-
version: "1.2.0-alpha-
|
|
54533
|
+
version: "1.2.0-alpha-901eb276429702cddd4bfe3a3656946f3ebc166a",
|
|
54475
54534
|
"private": true,
|
|
54476
54535
|
type: "module",
|
|
54477
54536
|
scripts: {
|
|
@@ -55866,6 +55925,83 @@ async function normalizeInitScriptSource(script, arg, caller = DEFAULT_CALLER) {
|
|
|
55866
55925
|
throw new TypeError(`${caller}: provide a string, function, or an object with content.`);
|
|
55867
55926
|
}
|
|
55868
55927
|
//#endregion
|
|
55928
|
+
//#region understudy/functionWait.ts
|
|
55929
|
+
/** Runs in the page's main world; keep this function self-contained. */
|
|
55930
|
+
function createFunctionWait(predicate, polling) {
|
|
55931
|
+
let stopped = false;
|
|
55932
|
+
let timer;
|
|
55933
|
+
let animationFrame;
|
|
55934
|
+
let resolve;
|
|
55935
|
+
let reject;
|
|
55936
|
+
const promise = new Promise((done, fail) => {
|
|
55937
|
+
resolve = done;
|
|
55938
|
+
reject = fail;
|
|
55939
|
+
});
|
|
55940
|
+
promise.catch(() => {});
|
|
55941
|
+
const stop = () => {
|
|
55942
|
+
stopped = true;
|
|
55943
|
+
clearTimeout(timer);
|
|
55944
|
+
if (animationFrame !== void 0) cancelAnimationFrame(animationFrame);
|
|
55945
|
+
};
|
|
55946
|
+
const poll = async () => {
|
|
55947
|
+
if (stopped) return;
|
|
55948
|
+
try {
|
|
55949
|
+
const evaluated = predicate();
|
|
55950
|
+
const value = await (typeof evaluated === "function" ? evaluated() : evaluated);
|
|
55951
|
+
if (stopped) return;
|
|
55952
|
+
if (value) {
|
|
55953
|
+
const serialized = JSON.stringify(value);
|
|
55954
|
+
if (serialized === void 0) throw new TypeError("waitForFunction result must be JSON-serializable");
|
|
55955
|
+
const result = JSON.parse(serialized);
|
|
55956
|
+
stop();
|
|
55957
|
+
resolve(result);
|
|
55958
|
+
} else if (polling === "raf") animationFrame = requestAnimationFrame(() => void poll());
|
|
55959
|
+
else timer = setTimeout(() => void poll(), polling);
|
|
55960
|
+
} catch (error) {
|
|
55961
|
+
stop();
|
|
55962
|
+
reject(error);
|
|
55963
|
+
}
|
|
55964
|
+
};
|
|
55965
|
+
poll();
|
|
55966
|
+
return {
|
|
55967
|
+
promise,
|
|
55968
|
+
dispose() {
|
|
55969
|
+
if (stopped) return;
|
|
55970
|
+
stop();
|
|
55971
|
+
reject(/* @__PURE__ */ new Error("Function wait disposed"));
|
|
55972
|
+
}
|
|
55973
|
+
};
|
|
55974
|
+
}
|
|
55975
|
+
async function waitForFunction(getFrame, expression, options = {}) {
|
|
55976
|
+
const polling = options.polling ?? "raf";
|
|
55977
|
+
if (polling !== "raf" && (!Number.isFinite(polling) || polling <= 0 || polling > 2147483647)) throw new RangeError("Polling must be 'raf' or a positive interval up to 2147483647ms");
|
|
55978
|
+
return runWithProgress({
|
|
55979
|
+
name: "waitForFunction",
|
|
55980
|
+
timeout: options.timeout ?? 0
|
|
55981
|
+
}, async (progress) => {
|
|
55982
|
+
for (;;) {
|
|
55983
|
+
progress.throwIfStopped();
|
|
55984
|
+
const frame = getFrame();
|
|
55985
|
+
try {
|
|
55986
|
+
const contextId = await frame.getMainWorldExecutionContextId(progress);
|
|
55987
|
+
if (getFrame() !== frame) continue;
|
|
55988
|
+
return await browserWait(frame.session, progress, (onLateResult) => progress.run("installing function wait", () => frame.session.send("Runtime.evaluate", {
|
|
55989
|
+
expression: `(${createFunctionWait.toString()})(() => (${expression}), ${JSON.stringify(polling)})`,
|
|
55990
|
+
contextId,
|
|
55991
|
+
awaitPromise: false,
|
|
55992
|
+
returnByValue: false
|
|
55993
|
+
}), onLateResult), "waiting for function");
|
|
55994
|
+
} catch (error) {
|
|
55995
|
+
progress.throwIfStopped();
|
|
55996
|
+
if (error instanceof BrowserWaitError) throw error;
|
|
55997
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55998
|
+
if (!message.includes("Cannot find context with specified id") && !message.includes("Execution context was destroyed") && !message.includes("Cannot find object with given id") && !message.includes("Inspected target navigated or closed") && !message.startsWith("main world not ready for frame ")) throw error;
|
|
55999
|
+
await progress.delay(25);
|
|
56000
|
+
}
|
|
56001
|
+
}
|
|
56002
|
+
});
|
|
56003
|
+
}
|
|
56004
|
+
//#endregion
|
|
55869
56005
|
//#region dom/screenshotScripts/resolveMaskRect.ts
|
|
55870
56006
|
function resolveMaskRect(maskToken) {
|
|
55871
56007
|
function safeClosest(el, selector) {
|
|
@@ -57424,6 +57560,14 @@ var Page = class Page {
|
|
|
57424
57560
|
async waitForTimeout(ms) {
|
|
57425
57561
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
57426
57562
|
}
|
|
57563
|
+
/** Poll an expression in the page's main world until its value is truthy. */
|
|
57564
|
+
async waitForFunction(expression, options) {
|
|
57565
|
+
return waitForFunction(() => {
|
|
57566
|
+
if (this.disposed) throw new Error("Page is disposed");
|
|
57567
|
+
if (!this.conn.connected) throw new Error("CDP connection closed: function wait stopped");
|
|
57568
|
+
return this.mainFrameWrapper;
|
|
57569
|
+
}, expression, options);
|
|
57570
|
+
}
|
|
57427
57571
|
/**
|
|
57428
57572
|
* Wait for an element matching the selector to appear in the DOM.
|
|
57429
57573
|
* Uses MutationObserver for efficiency
|
|
@@ -58633,6 +58777,9 @@ var StagehandRuntime = class {
|
|
|
58633
58777
|
async pageWaitForSelector(params) {
|
|
58634
58778
|
return { matched: await this.resolvePage(params.pageId).waitForSelector(params.selector, params.options) };
|
|
58635
58779
|
}
|
|
58780
|
+
async pageWaitForFunction(params) {
|
|
58781
|
+
return { value: await this.resolvePage(params.pageId).waitForFunction(params.expression, params.options) };
|
|
58782
|
+
}
|
|
58636
58783
|
async pageScreenshot(params) {
|
|
58637
58784
|
const page = this.resolvePage(params.pageId);
|
|
58638
58785
|
let options;
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/input_types.py
RENAMED
|
@@ -277,6 +277,9 @@ FieldSchema13: TypeAlias = Optional[Union[str, float, bool, list["FieldSchema13"
|
|
|
277
277
|
FieldSchema14: TypeAlias = Optional[Union[str, float, bool, list["FieldSchema14"], dict[str, "FieldSchema14"]]]
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
FieldSchema15: TypeAlias = Optional[Union[str, float, bool, list["FieldSchema15"], dict[str, "FieldSchema15"]]]
|
|
281
|
+
|
|
282
|
+
|
|
280
283
|
FieldSchema2: TypeAlias = Optional[Union[str, float, bool, list["FieldSchema2"], dict[str, "FieldSchema2"]]]
|
|
281
284
|
|
|
282
285
|
|
|
@@ -332,7 +335,7 @@ class InputFilePayload(TypedDict):
|
|
|
332
335
|
class JSONRPCErrorObject(TypedDict):
|
|
333
336
|
code: int
|
|
334
337
|
message: str
|
|
335
|
-
data: NotRequired[
|
|
338
|
+
data: NotRequired[FieldSchema15]
|
|
336
339
|
|
|
337
340
|
|
|
338
341
|
JSONRPCRequestId: TypeAlias = int
|
|
@@ -718,7 +721,7 @@ class PageAddInitScriptParams(TypedDict):
|
|
|
718
721
|
source: str
|
|
719
722
|
|
|
720
723
|
|
|
721
|
-
PageCDPEventParams: TypeAlias = dict[str,
|
|
724
|
+
PageCDPEventParams: TypeAlias = dict[str, FieldSchema13]
|
|
722
725
|
|
|
723
726
|
|
|
724
727
|
class PageCDPEvent(TypedDict):
|
|
@@ -984,6 +987,21 @@ class PageVoidResult(TypedDict):
|
|
|
984
987
|
ok: Literal[True]
|
|
985
988
|
|
|
986
989
|
|
|
990
|
+
class PageWaitForFunctionOptions(TypedDict):
|
|
991
|
+
polling: NotRequired[Literal["raf"] | float]
|
|
992
|
+
timeout: NotRequired[int]
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
class PageWaitForFunctionParams(TypedDict):
|
|
996
|
+
page_id: str
|
|
997
|
+
expression: str
|
|
998
|
+
options: NotRequired[PageWaitForFunctionOptions]
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class PageWaitForFunctionResult(TypedDict):
|
|
1002
|
+
value: FieldSchema10
|
|
1003
|
+
|
|
1004
|
+
|
|
987
1005
|
class PageWaitForLoadStateParams(TypedDict):
|
|
988
1006
|
page_id: str
|
|
989
1007
|
state: LoadState
|
|
@@ -1101,7 +1119,7 @@ class StagehandInitResult(TypedDict):
|
|
|
1101
1119
|
pages: list[PageRef]
|
|
1102
1120
|
|
|
1103
1121
|
|
|
1104
|
-
StagehandLogData: TypeAlias = dict[str,
|
|
1122
|
+
StagehandLogData: TypeAlias = dict[str, FieldSchema12]
|
|
1105
1123
|
|
|
1106
1124
|
|
|
1107
1125
|
StagehandLogLevel: TypeAlias = Literal["debug", "info", "warn", "error"]
|
|
@@ -1242,7 +1260,7 @@ class WebMCPAnnotation(TypedDict):
|
|
|
1242
1260
|
WebMCPInvocationStatus: TypeAlias = Literal["Completed", "Canceled", "Error"]
|
|
1243
1261
|
|
|
1244
1262
|
|
|
1245
|
-
WebMCPJsonValue: TypeAlias =
|
|
1263
|
+
WebMCPJsonValue: TypeAlias = FieldSchema11
|
|
1246
1264
|
|
|
1247
1265
|
|
|
1248
1266
|
class PageWebMCPInvokeToolParams(TypedDict):
|
|
@@ -710,6 +710,12 @@ class FieldSchema14(
|
|
|
710
710
|
root: Optional[Union[StrictStr, StrictInt, StrictFloat, StrictBool, list[Optional["FieldSchema14"]], dict[StrictStr, Optional["FieldSchema14"]]]]
|
|
711
711
|
|
|
712
712
|
|
|
713
|
+
class FieldSchema15(
|
|
714
|
+
RootModel[Optional[Union[StrictStr, StrictInt, StrictFloat, StrictBool, list[Optional["FieldSchema15"]], dict[StrictStr, Optional["FieldSchema15"]]]]]
|
|
715
|
+
):
|
|
716
|
+
root: Optional[Union[StrictStr, StrictInt, StrictFloat, StrictBool, list[Optional["FieldSchema15"]], dict[StrictStr, Optional["FieldSchema15"]]]]
|
|
717
|
+
|
|
718
|
+
|
|
713
719
|
class FieldSchema2(
|
|
714
720
|
RootModel[Optional[Union[StrictStr, StrictInt, StrictFloat, StrictBool, list[Optional["FieldSchema2"]], dict[StrictStr, Optional["FieldSchema2"]]]]]
|
|
715
721
|
):
|
|
@@ -811,7 +817,7 @@ class JSONRPCErrorObject(WireModel):
|
|
|
811
817
|
)
|
|
812
818
|
code: Annotated[StrictInt, Field(ge=-9007199254740991, le=9007199254740991)]
|
|
813
819
|
message: StrictStr
|
|
814
|
-
data: Optional[
|
|
820
|
+
data: Optional[FieldSchema15] = None
|
|
815
821
|
|
|
816
822
|
|
|
817
823
|
class JSONRPCRequestId(RootModel[StrictInt]):
|
|
@@ -1564,8 +1570,8 @@ class PageCDPEventNotification(WireModel):
|
|
|
1564
1570
|
event: PageCDPEvent
|
|
1565
1571
|
|
|
1566
1572
|
|
|
1567
|
-
class PageCDPEventParams(RootModel[dict[StrictStr, Optional[
|
|
1568
|
-
root: dict[StrictStr, Optional[
|
|
1573
|
+
class PageCDPEventParams(RootModel[dict[StrictStr, Optional[FieldSchema13]]]):
|
|
1574
|
+
root: dict[StrictStr, Optional[FieldSchema13]]
|
|
1569
1575
|
|
|
1570
1576
|
|
|
1571
1577
|
class PageClickOptions(WireModel):
|
|
@@ -2027,6 +2033,33 @@ class PageVoidResult(WireModel):
|
|
|
2027
2033
|
ok: Literal[True]
|
|
2028
2034
|
|
|
2029
2035
|
|
|
2036
|
+
class PageWaitForFunctionOptions(WireModel):
|
|
2037
|
+
model_config = ConfigDict(
|
|
2038
|
+
extra="forbid",
|
|
2039
|
+
validate_by_name=True,
|
|
2040
|
+
)
|
|
2041
|
+
polling: Optional[Union[Literal["raf"], Polling]] = None
|
|
2042
|
+
timeout: Annotated[Optional[StrictInt], Field(ge=0, le=9007199254740991)] = None
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
class PageWaitForFunctionParams(WireModel):
|
|
2046
|
+
model_config = ConfigDict(
|
|
2047
|
+
extra="forbid",
|
|
2048
|
+
validate_by_name=True,
|
|
2049
|
+
)
|
|
2050
|
+
page_id: StrictStr
|
|
2051
|
+
expression: StrictStr
|
|
2052
|
+
options: Optional[PageWaitForFunctionOptions] = None
|
|
2053
|
+
|
|
2054
|
+
|
|
2055
|
+
class PageWaitForFunctionResult(WireModel):
|
|
2056
|
+
model_config = ConfigDict(
|
|
2057
|
+
extra="forbid",
|
|
2058
|
+
validate_by_name=True,
|
|
2059
|
+
)
|
|
2060
|
+
value: Optional[FieldSchema10]
|
|
2061
|
+
|
|
2062
|
+
|
|
2030
2063
|
class PageWaitForLoadStateParams(WireModel):
|
|
2031
2064
|
model_config = ConfigDict(
|
|
2032
2065
|
extra="forbid",
|
|
@@ -2123,6 +2156,10 @@ class PageWebMCPToolsResult(WireModel):
|
|
|
2123
2156
|
tools: list[WebMCPToolDescriptor]
|
|
2124
2157
|
|
|
2125
2158
|
|
|
2159
|
+
class Polling(RootModel[StrictFloat]):
|
|
2160
|
+
root: Annotated[StrictFloat, Field(gt=0.0, le=2147483647.0)]
|
|
2161
|
+
|
|
2162
|
+
|
|
2126
2163
|
class ProxyConfig(RootModel[Union[BrowserbaseProxyConfig, ExternalProxyConfig]]):
|
|
2127
2164
|
root: Union[BrowserbaseProxyConfig, ExternalProxyConfig]
|
|
2128
2165
|
|
|
@@ -2321,8 +2358,8 @@ class StagehandLog(WireModel):
|
|
|
2321
2358
|
data: StagehandLogData
|
|
2322
2359
|
|
|
2323
2360
|
|
|
2324
|
-
class StagehandLogData(RootModel[dict[StrictStr, Optional[
|
|
2325
|
-
root: dict[StrictStr, Optional[
|
|
2361
|
+
class StagehandLogData(RootModel[dict[StrictStr, Optional[FieldSchema12]]]):
|
|
2362
|
+
root: dict[StrictStr, Optional[FieldSchema12]]
|
|
2326
2363
|
|
|
2327
2364
|
|
|
2328
2365
|
class StagehandLogLevel(StrEnum):
|
|
@@ -2481,8 +2518,8 @@ class WebMCPInvocationStatus(StrEnum):
|
|
|
2481
2518
|
error = "Error"
|
|
2482
2519
|
|
|
2483
2520
|
|
|
2484
|
-
class WebMCPJsonValue(RootModel[Optional[
|
|
2485
|
-
root: Optional[
|
|
2521
|
+
class WebMCPJsonValue(RootModel[Optional[FieldSchema11]]):
|
|
2522
|
+
root: Optional[FieldSchema11]
|
|
2486
2523
|
|
|
2487
2524
|
|
|
2488
2525
|
class WebMCPRemoteObject(RootModel[dict[StrictStr, WebMCPJsonValue]]):
|
|
@@ -2548,6 +2585,7 @@ FieldSchema11.model_rebuild()
|
|
|
2548
2585
|
FieldSchema12.model_rebuild()
|
|
2549
2586
|
FieldSchema13.model_rebuild()
|
|
2550
2587
|
FieldSchema14.model_rebuild()
|
|
2588
|
+
FieldSchema15.model_rebuild()
|
|
2551
2589
|
FieldSchema2.model_rebuild()
|
|
2552
2590
|
FieldSchema3.model_rebuild()
|
|
2553
2591
|
FieldSchema4.model_rebuild()
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/protocol_version.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"""Generated from packages/protocol/package.json. Do not edit."""
|
|
2
2
|
|
|
3
3
|
PROTOCOL_SEMVER_PATTERN = '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$'
|
|
4
|
-
STAGEHAND_PROTOCOL_VERSION = '3.0.0-alpha-
|
|
4
|
+
STAGEHAND_PROTOCOL_VERSION = '3.0.0-alpha-901eb276429702cddd4bfe3a3656946f3ebc166a'
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from collections.abc import Mapping
|
|
2
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated, cast
|
|
3
4
|
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
|
-
from pydantic import
|
|
6
|
+
from pydantic import (
|
|
7
|
+
AfterValidator,
|
|
8
|
+
AnyUrl,
|
|
9
|
+
BaseModel,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
TypeAdapter,
|
|
12
|
+
ValidationError,
|
|
13
|
+
ValidatorFunctionWrapHandler,
|
|
14
|
+
WrapValidator,
|
|
15
|
+
model_validator,
|
|
16
|
+
)
|
|
6
17
|
from typing_extensions import override
|
|
7
18
|
|
|
8
19
|
_url_adapter = TypeAdapter(AnyUrl)
|
|
@@ -15,6 +26,54 @@ def _validate_url(value: str) -> str:
|
|
|
15
26
|
|
|
16
27
|
WireUrl = Annotated[str, AfterValidator(_validate_url)]
|
|
17
28
|
|
|
29
|
+
# RFC 9110 field names are tokens. Restrict values to ASCII so HTTP discovery
|
|
30
|
+
# and WebSocket handshakes encode them identically; allow only internal whitespace.
|
|
31
|
+
_HEADER_NAME = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
|
|
32
|
+
_HEADER_VALUE = re.compile(r"[\t\x20-\x7e]*")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _validate_headers(value: Mapping[str, str]) -> Mapping[str, str]:
|
|
36
|
+
for name, header_value in value.items():
|
|
37
|
+
if not _HEADER_NAME.fullmatch(name):
|
|
38
|
+
raise ValueError
|
|
39
|
+
if not _HEADER_VALUE.fullmatch(header_value) or header_value != header_value.strip(" \t"):
|
|
40
|
+
raise ValueError
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _redact_header_validation_errors(
|
|
45
|
+
value: object, handler: ValidatorFunctionWrapHandler
|
|
46
|
+
) -> dict[str, str]:
|
|
47
|
+
try:
|
|
48
|
+
return cast(dict[str, str], handler(value))
|
|
49
|
+
except ValidationError:
|
|
50
|
+
# Error inputs, locations, and validator context can all contain credentials.
|
|
51
|
+
# Rebuild the error rather than only hiding inputs in its formatted message.
|
|
52
|
+
raise ValidationError.from_exception_data(
|
|
53
|
+
"CDP headers",
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
"type": "value_error",
|
|
57
|
+
"loc": (),
|
|
58
|
+
"input": "[redacted]",
|
|
59
|
+
"ctx": {
|
|
60
|
+
"error": ValueError(
|
|
61
|
+
"headers must map valid HTTP header names to ASCII string values "
|
|
62
|
+
"without leading or trailing spaces or tabs"
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
hide_input=True,
|
|
68
|
+
) from None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
WireHeaders = Annotated[
|
|
72
|
+
dict[str, str],
|
|
73
|
+
AfterValidator(_validate_headers),
|
|
74
|
+
WrapValidator(_redact_header_validation_errors),
|
|
75
|
+
]
|
|
76
|
+
|
|
18
77
|
|
|
19
78
|
def _validate_websocket_url(value: str) -> str:
|
|
20
79
|
parsed = urlparse(value)
|
|
@@ -312,12 +312,14 @@ async def _connect_browser(
|
|
|
312
312
|
preloaded_extension: bool = False,
|
|
313
313
|
after_connect: Callable[[CDPClient], Awaitable[None]] | None = None,
|
|
314
314
|
worker_init_metadata: _WorkerInitMetadata,
|
|
315
|
+
headers: Mapping[str, str] | None = None,
|
|
315
316
|
) -> StagehandBrowser:
|
|
316
317
|
owns_source = origin == "launched" and not source.keep_alive
|
|
317
318
|
cdp_client: CDPClient | None = None
|
|
318
319
|
try:
|
|
319
320
|
cdp_client = await CDPClient.connect(
|
|
320
321
|
cdp_url=source.cdp_url,
|
|
322
|
+
**({"headers": headers} if headers else {}),
|
|
321
323
|
extension_dir=extension_dir,
|
|
322
324
|
local_extension_dir=local_extension_dir,
|
|
323
325
|
extension_id=extension_id,
|
|
@@ -536,6 +538,7 @@ class LocalBrowser:
|
|
|
536
538
|
cdp_url: str,
|
|
537
539
|
extension_id: str | None = None,
|
|
538
540
|
extension_cdp_url: str | None = None,
|
|
541
|
+
headers: Mapping[str, str] | None = None,
|
|
539
542
|
) -> StagehandBrowser:
|
|
540
543
|
"""Connect to an existing browser.
|
|
541
544
|
|
|
@@ -547,6 +550,10 @@ class LocalBrowser:
|
|
|
547
550
|
client connected to. Set it when the client reaches Chrome through
|
|
548
551
|
a proxy the browser cannot or should not use, such as an
|
|
549
552
|
authenticated gateway whose URL carries credentials.
|
|
553
|
+
headers: HTTP headers to send with the CDP connection, such as a
|
|
554
|
+
gateway's credentials, so they need not ride in `cdp_url`. They
|
|
555
|
+
go to `cdp_url` and to the WebSocket it advertises when that is
|
|
556
|
+
on the same host, never elsewhere, and never to the extension.
|
|
550
557
|
"""
|
|
551
558
|
options = LocalBrowserConnectOptions.model_validate({
|
|
552
559
|
name: value
|
|
@@ -554,6 +561,7 @@ class LocalBrowser:
|
|
|
554
561
|
("cdp_url", cdp_url),
|
|
555
562
|
("extension_id", extension_id),
|
|
556
563
|
("extension_cdp_url", extension_cdp_url),
|
|
564
|
+
("headers", dict(headers) if isinstance(headers, Mapping) else headers),
|
|
557
565
|
)
|
|
558
566
|
if value is not None
|
|
559
567
|
})
|
|
@@ -567,6 +575,7 @@ class LocalBrowser:
|
|
|
567
575
|
browser=None,
|
|
568
576
|
browser_cdp_url=options.extension_cdp_url,
|
|
569
577
|
),
|
|
578
|
+
headers=options.headers,
|
|
570
579
|
)
|
|
571
580
|
|
|
572
581
|
|
|
@@ -2,19 +2,28 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
5
6
|
import re
|
|
6
7
|
import time
|
|
7
8
|
from collections.abc import Callable, Mapping
|
|
8
9
|
from contextlib import suppress
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from typing import Any, Literal, Protocol, cast
|
|
11
|
-
from urllib.
|
|
12
|
+
from urllib.error import HTTPError
|
|
13
|
+
from urllib.parse import urljoin, urlsplit
|
|
14
|
+
from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen
|
|
15
|
+
|
|
16
|
+
from typing_extensions import override
|
|
17
|
+
from websockets.asyncio.client import connect as WebSocketConnect
|
|
18
|
+
from websockets.exceptions import InvalidStatus
|
|
12
19
|
|
|
13
20
|
from stagehand._generated.protocol_version import (
|
|
14
21
|
PROTOCOL_SEMVER_PATTERN,
|
|
15
22
|
STAGEHAND_PROTOCOL_VERSION,
|
|
16
23
|
)
|
|
17
24
|
|
|
25
|
+
_LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
18
27
|
STAGEHAND_SEND_TO_HOST_BINDING = "__stagehandSendToHost"
|
|
19
28
|
_RUNTIME_NAME = "stagehand"
|
|
20
29
|
_STAGEHAND_EXTENSION_NAME = "Stagehand Runtime"
|
|
@@ -257,6 +266,7 @@ class CDPClient:
|
|
|
257
266
|
extension_id: str | None = None,
|
|
258
267
|
preloaded_extension: bool = False,
|
|
259
268
|
service_worker_url_includes: str | None = None,
|
|
269
|
+
headers: Mapping[str, str] | None = None,
|
|
260
270
|
) -> CDPClient:
|
|
261
271
|
"""Connect without an independent lifecycle deadline.
|
|
262
272
|
|
|
@@ -278,8 +288,16 @@ class CDPClient:
|
|
|
278
288
|
"or preloaded_extension is required"
|
|
279
289
|
)
|
|
280
290
|
|
|
281
|
-
web_socket_debugger_url = await _resolve_browser_web_socket_url(cdp_url)
|
|
282
|
-
|
|
291
|
+
web_socket_debugger_url = await _resolve_browser_web_socket_url(cdp_url, headers=headers)
|
|
292
|
+
web_socket_headers = headers or None
|
|
293
|
+
if web_socket_headers and not _can_forward_headers(cdp_url, web_socket_debugger_url):
|
|
294
|
+
web_socket_headers = None
|
|
295
|
+
_LOGGER.debug(
|
|
296
|
+
"CDP headers were withheld from the advertised WebSocket endpoint because "
|
|
297
|
+
"its host or port differs, or forwarding would downgrade transport security. "
|
|
298
|
+
"If the handshake is rejected, check the gateway's advertised CDP endpoint."
|
|
299
|
+
)
|
|
300
|
+
socket = await _connect_web_socket(web_socket_debugger_url, headers=web_socket_headers)
|
|
283
301
|
client = cls(socket, web_socket_debugger_url)
|
|
284
302
|
|
|
285
303
|
try:
|
|
@@ -659,33 +677,103 @@ class CDPClient:
|
|
|
659
677
|
task.add_done_callback(finish)
|
|
660
678
|
|
|
661
679
|
|
|
662
|
-
|
|
663
|
-
|
|
680
|
+
class _UnsafeCDPRedirectError(RuntimeError):
|
|
681
|
+
"""A redirect would expose caller headers to an unauthorized endpoint."""
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
_DEFAULT_PORTS = {"http": 80, "ws": 80, "https": 443, "wss": 443}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _can_forward_headers(source: str, destination: str) -> bool:
|
|
688
|
+
"""Require the same host and effective port, without a TLS downgrade."""
|
|
689
|
+
try:
|
|
690
|
+
first, second = urlsplit(source), urlsplit(destination)
|
|
691
|
+
return (
|
|
692
|
+
first.scheme in _DEFAULT_PORTS
|
|
693
|
+
and second.scheme in _DEFAULT_PORTS
|
|
694
|
+
and first.hostname is not None
|
|
695
|
+
and first.hostname == second.hostname
|
|
696
|
+
and (first.port if first.port is not None else _DEFAULT_PORTS[first.scheme])
|
|
697
|
+
== (second.port if second.port is not None else _DEFAULT_PORTS[second.scheme])
|
|
698
|
+
and not (first.scheme in ("https", "wss") and second.scheme in ("http", "ws"))
|
|
699
|
+
)
|
|
700
|
+
except ValueError:
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class _CDPRedirectHandler(HTTPRedirectHandler):
|
|
705
|
+
@override
|
|
706
|
+
def redirect_request(
|
|
707
|
+
self, req: Request, fp: Any, code: int, msg: str, headers: Any, newurl: str
|
|
708
|
+
) -> Request | None:
|
|
709
|
+
if not _can_forward_headers(req.full_url, newurl):
|
|
710
|
+
raise _UnsafeCDPRedirectError(
|
|
711
|
+
"Refusing CDP HTTP redirect with headers to a different host or port, "
|
|
712
|
+
"or an insecure endpoint"
|
|
713
|
+
)
|
|
714
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class _CDPWebSocketConnect(WebSocketConnect):
|
|
718
|
+
@override
|
|
719
|
+
def process_redirect(self, exc: Exception) -> Exception | str:
|
|
720
|
+
if (
|
|
721
|
+
self.additional_headers
|
|
722
|
+
and isinstance(exc, InvalidStatus)
|
|
723
|
+
and exc.response.status_code in (300, 301, 302, 303, 307, 308)
|
|
724
|
+
and "Location" in exc.response.headers
|
|
725
|
+
and not _can_forward_headers(
|
|
726
|
+
self.uri, urljoin(self.uri, exc.response.headers["Location"])
|
|
727
|
+
)
|
|
728
|
+
):
|
|
729
|
+
return _UnsafeCDPRedirectError(
|
|
730
|
+
"Refusing CDP WebSocket redirect with headers to a different host or port, "
|
|
731
|
+
"or an insecure endpoint"
|
|
732
|
+
)
|
|
733
|
+
return super().process_redirect(exc)
|
|
734
|
+
|
|
664
735
|
|
|
736
|
+
async def _connect_web_socket(url: str, *, headers: Mapping[str, str] | None = None) -> _WebSocket:
|
|
665
737
|
return cast(
|
|
666
738
|
_WebSocket,
|
|
667
|
-
await
|
|
739
|
+
await _CDPWebSocketConnect(
|
|
740
|
+
url, open_timeout=None, max_size=None, additional_headers=headers
|
|
741
|
+
),
|
|
668
742
|
)
|
|
669
743
|
|
|
670
744
|
|
|
671
|
-
async def _resolve_browser_web_socket_url(
|
|
745
|
+
async def _resolve_browser_web_socket_url(
|
|
746
|
+
cdp_url: str, *, headers: Mapping[str, str] | None = None
|
|
747
|
+
) -> str:
|
|
672
748
|
if cdp_url.startswith(("ws://", "wss://")):
|
|
673
749
|
return cdp_url
|
|
674
750
|
|
|
675
751
|
base_url = cdp_url.rstrip("/")
|
|
676
752
|
while True:
|
|
677
753
|
try:
|
|
678
|
-
version = await asyncio.to_thread(_read_json, f"{base_url}/json/version")
|
|
754
|
+
version = await asyncio.to_thread(_read_json, f"{base_url}/json/version", headers)
|
|
679
755
|
debugger_url = version.get("webSocketDebuggerUrl")
|
|
680
756
|
if isinstance(debugger_url, str) and debugger_url:
|
|
681
757
|
return debugger_url
|
|
758
|
+
except _UnsafeCDPRedirectError:
|
|
759
|
+
raise
|
|
760
|
+
except HTTPError as error:
|
|
761
|
+
error.close()
|
|
762
|
+
if error.code in (401, 403):
|
|
763
|
+
raise RuntimeError(
|
|
764
|
+
f"CDP discovery was rejected with HTTP {error.code}. "
|
|
765
|
+
"Check the gateway credentials and access permissions."
|
|
766
|
+
) from None
|
|
682
767
|
except Exception:
|
|
683
768
|
pass
|
|
684
769
|
await asyncio.sleep(0.25)
|
|
685
770
|
|
|
686
771
|
|
|
687
|
-
def _read_json(url: str) -> dict[str, object]:
|
|
688
|
-
|
|
772
|
+
def _read_json(url: str, headers: Mapping[str, str] | None = None) -> dict[str, object]:
|
|
773
|
+
request = Request(url, headers=dict(headers or {}))
|
|
774
|
+
# Keep the existing redirect behavior for connections without caller headers.
|
|
775
|
+
open_url = build_opener(_CDPRedirectHandler()).open if headers else urlopen
|
|
776
|
+
with open_url(request, timeout=2) as response:
|
|
689
777
|
value: Any = json.load(response)
|
|
690
778
|
if not isinstance(value, dict):
|
|
691
779
|
raise RuntimeError("CDP version endpoint returned invalid JSON")
|
|
@@ -18,7 +18,7 @@ from ._generated.models import (
|
|
|
18
18
|
StagehandResultMetadata,
|
|
19
19
|
TelemetryConfig,
|
|
20
20
|
)
|
|
21
|
-
from ._validation import WireModel, WireUrl, WireWebSocketUrl
|
|
21
|
+
from ._validation import WireHeaders, WireModel, WireUrl, WireWebSocketUrl
|
|
22
22
|
from .browserbase_session import DEFAULT_BROWSERBASE_URL
|
|
23
23
|
from .client_types import Cache as CacheInput
|
|
24
24
|
|
|
@@ -103,13 +103,16 @@ class LocalBrowserLaunchOptions(WireModel):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
class LocalBrowserConnectOptions(WireModel):
|
|
106
|
-
|
|
106
|
+
# Headers may carry credentials, so validation errors never echo the input.
|
|
107
|
+
model_config = ConfigDict(extra="forbid", strict=True, hide_input_in_errors=True)
|
|
107
108
|
|
|
108
109
|
cdp_url: Annotated[str, Field(min_length=1)]
|
|
109
110
|
extension_id: str | None = None
|
|
110
111
|
"""Deprecated and ignored. Omit this field; Stagehand discovers the extension automatically."""
|
|
111
112
|
extension_cdp_url: WireWebSocketUrl | None = None
|
|
112
113
|
"""Browser-level CDP WebSocket the extension dials, when it differs from `cdp_url`."""
|
|
114
|
+
headers: WireHeaders | None = None
|
|
115
|
+
"""HTTP headers sent with the CDP connection, e.g. a gateway's credentials."""
|
|
113
116
|
|
|
114
117
|
|
|
115
118
|
class BrowserbaseClientOptions(WireModel):
|
|
@@ -69,6 +69,8 @@ class LocalBrowserConnectOptions(TypedDict):
|
|
|
69
69
|
"""Deprecated and ignored. Omit this field; Stagehand discovers the extension automatically."""
|
|
70
70
|
extension_cdp_url: NotRequired[str]
|
|
71
71
|
"""Browser-level CDP WebSocket the extension dials, when it differs from `cdp_url`."""
|
|
72
|
+
headers: NotRequired[dict[str, str]]
|
|
73
|
+
"""HTTP headers sent with the CDP connection, e.g. a gateway's credentials."""
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
class BrowserbaseClientOptions(TypedDict, total=False):
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import base64
|
|
5
5
|
import builtins
|
|
6
6
|
import inspect
|
|
7
|
+
import json
|
|
7
8
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Literal, TypeVar, cast, overload
|
|
@@ -67,6 +68,9 @@ from ._generated.models import (
|
|
|
67
68
|
PageTypeParams,
|
|
68
69
|
PageUrlResult,
|
|
69
70
|
PageVoidResult,
|
|
71
|
+
PageWaitForFunctionOptions,
|
|
72
|
+
PageWaitForFunctionParams,
|
|
73
|
+
PageWaitForFunctionResult,
|
|
70
74
|
PageWaitForLoadStateParams,
|
|
71
75
|
PageWaitForSelectorOptions,
|
|
72
76
|
PageWaitForSelectorParams,
|
|
@@ -94,6 +98,9 @@ ToolsAddedListener = Callable[[list[WebMCPTool]], object | Awaitable[object]]
|
|
|
94
98
|
ToolsRemovedListener = Callable[[list[WebMCPToolIdentity]], object | Awaitable[object]]
|
|
95
99
|
NotificationT = TypeVar("NotificationT", PageCDPEventNotification, PageEventNotification)
|
|
96
100
|
|
|
101
|
+
# Distinguishes an omitted wait_for_function ``arg`` from an explicit JSON null.
|
|
102
|
+
_UNSET_ARG = object()
|
|
103
|
+
|
|
97
104
|
|
|
98
105
|
class CDPSubscription:
|
|
99
106
|
def __init__(
|
|
@@ -539,6 +546,44 @@ class Page:
|
|
|
539
546
|
PageVoidResult,
|
|
540
547
|
)
|
|
541
548
|
|
|
549
|
+
async def wait_for_function(
|
|
550
|
+
self,
|
|
551
|
+
expression: str,
|
|
552
|
+
*,
|
|
553
|
+
arg: JsonValue = cast(JsonValue, _UNSET_ARG),
|
|
554
|
+
polling: Literal["raf"] | float | None = None,
|
|
555
|
+
timeout: int | None = None,
|
|
556
|
+
) -> JsonValue:
|
|
557
|
+
"""Poll JavaScript in the page until it returns a truthy JSON value."""
|
|
558
|
+
if arg is not _UNSET_ARG:
|
|
559
|
+
try:
|
|
560
|
+
serialized_arg = json.dumps(arg, allow_nan=False)
|
|
561
|
+
except (TypeError, ValueError) as error:
|
|
562
|
+
message = "page.wait_for_function: 'arg' must be JSON-serializable."
|
|
563
|
+
raise TypeError(message) from error
|
|
564
|
+
# Like Playwright, a function expression receives arg; other expressions ignore it.
|
|
565
|
+
# The browser invokes the returned thunk once per poll. JSON.parse keeps own
|
|
566
|
+
# "__proto__" keys that an object literal would treat as a prototype.
|
|
567
|
+
parsed_arg = f"JSON.parse({json.dumps(serialized_arg)})"
|
|
568
|
+
expression = (
|
|
569
|
+
f'((value) => typeof value === "function" ? () => value({parsed_arg}) : value)'
|
|
570
|
+
f"(({expression}))"
|
|
571
|
+
)
|
|
572
|
+
params = PageWaitForFunctionParams(page_id=self.page_id, expression=expression)
|
|
573
|
+
options = PageWaitForFunctionOptions.model_validate({
|
|
574
|
+
name: value
|
|
575
|
+
for name, value in (("polling", polling), ("timeout", timeout))
|
|
576
|
+
if value is not None
|
|
577
|
+
})
|
|
578
|
+
if options.model_fields_set:
|
|
579
|
+
params.options = options
|
|
580
|
+
result = await self._rpc_client.send(
|
|
581
|
+
"page.wait_for_function",
|
|
582
|
+
params,
|
|
583
|
+
PageWaitForFunctionResult,
|
|
584
|
+
)
|
|
585
|
+
return None if result.value is None else cast(JsonValue, result.value.model_dump())
|
|
586
|
+
|
|
542
587
|
async def wait_for_selector(
|
|
543
588
|
self,
|
|
544
589
|
selector: str,
|
|
@@ -36,6 +36,7 @@ _DEFAULT_OPERATION_TIMEOUT_MS = {
|
|
|
36
36
|
"page.webmcp_tools": 1_000,
|
|
37
37
|
}
|
|
38
38
|
_UNBOUNDED_BY_DEFAULT_METHODS = {
|
|
39
|
+
"page.wait_for_function",
|
|
39
40
|
"stagehand.init",
|
|
40
41
|
"stagehand.close",
|
|
41
42
|
"stagehand.act",
|
|
@@ -602,6 +603,7 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
|
|
|
602
603
|
"page.screenshot",
|
|
603
604
|
"page.pdf",
|
|
604
605
|
"page.snapshot",
|
|
606
|
+
"page.wait_for_function",
|
|
605
607
|
"page.wait_for_selector",
|
|
606
608
|
"page.webmcp_tools",
|
|
607
609
|
"page.webmcp_invocation_result",
|
|
@@ -613,10 +615,12 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
|
|
|
613
615
|
operation_timeout_ms = _numeric_property(params, "ms")
|
|
614
616
|
|
|
615
617
|
if operation_timeout_ms is not None:
|
|
616
|
-
if
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
618
|
+
if operation_timeout_ms == 0 and method in {
|
|
619
|
+
"page.pdf",
|
|
620
|
+
"page.snapshot",
|
|
621
|
+
"page.wait_for_function",
|
|
622
|
+
"page.wait_for_selector",
|
|
623
|
+
}:
|
|
620
624
|
return None
|
|
621
625
|
return (_RPC_RESPONSE_GRACE_MS + max(0, operation_timeout_ms)) / 1_000
|
|
622
626
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/content-script.js
RENAMED
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_extension/manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/_generated/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_services.py
RENAMED
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_session.py
RENAMED
|
File without changes
|
{stagehand-4.3.0a0.dev1620 → stagehand-4.3.0a0.dev1626}/src/stagehand/browserbase_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|