stagehand 4.4.0a0.dev1673__tar.gz → 4.4.0a0.dev1676__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.
Files changed (39) hide show
  1. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/PKG-INFO +1 -1
  2. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/pyproject.toml +1 -1
  3. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/manifest.json +2 -1
  4. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/service-worker.js +262 -230
  5. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_generated/protocol_version.py +1 -1
  6. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/README.md +0 -0
  7. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/__init__.py +0 -0
  8. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/blank.html +0 -0
  9. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/content-script.js +0 -0
  10. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
  11. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
  12. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/wake-service-worker.html +0 -0
  13. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_extension/wake-service-worker.js +0 -0
  14. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_generated/__init__.py +0 -0
  15. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_generated/input_types.py +0 -0
  16. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_generated/models.py +0 -0
  17. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_sdk_identity.py +0 -0
  18. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/_validation.py +0 -0
  19. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browser.py +0 -0
  20. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browser_clipboard.py +0 -0
  21. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browser_context.py +0 -0
  22. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browserbase_connection.py +0 -0
  23. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browserbase_rpc.py +0 -0
  24. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browserbase_services.py +0 -0
  25. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browserbase_session.py +0 -0
  26. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/browserbase_transport.py +0 -0
  27. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/cdp_client.py +0 -0
  28. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/client_models.py +0 -0
  29. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/client_types.py +0 -0
  30. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/extension_assets.py +0 -0
  31. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/file_upload.py +0 -0
  32. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/locator.py +0 -0
  33. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/page.py +0 -0
  34. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/py.typed +0 -0
  35. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/response.py +0 -0
  36. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/rpc_client.py +0 -0
  37. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/stagehand.py +0 -0
  38. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/timeouts.py +0 -0
  39. {stagehand-4.4.0a0.dev1673 → stagehand-4.4.0a0.dev1676}/src/stagehand/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagehand
3
- Version: 4.4.0a0.dev1673
3
+ Version: 4.4.0a0.dev1676
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stagehand"
3
- version = "4.4.0a0.dev1673"
3
+ version = "4.4.0a0.dev1676"
4
4
  description = "Browserbase's SDK for building browser agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -6,7 +6,8 @@
6
6
  "debugger",
7
7
  "offscreen",
8
8
  "scripting",
9
- "tabs"
9
+ "tabs",
10
+ "declarativeNetRequest"
10
11
  ],
11
12
  "host_permissions": [
12
13
  "<all_urls>"
@@ -9206,7 +9206,7 @@ function isRecord$2(value) {
9206
9206
  }
9207
9207
  var package_default$1 = {
9208
9208
  name: "@browserbasehq/stagehand-protocol",
9209
- version: "3.1.1-alpha-1e50db680f0429b3fb96e0519cb6d9de8c4640a5",
9209
+ version: "3.1.1-alpha-c82e6170c29ab6fb17c4a10e44812bd29507e7d4",
9210
9210
  "private": true,
9211
9211
  type: "module",
9212
9212
  exports: {
@@ -18454,18 +18454,6 @@ async function waitForDomNetworkQuiet(frame, logger, timeout, progress) {
18454
18454
  if (remainingBudget === 0) return;
18455
18455
  await progress.run("enabling network events", () => client.send("Network.enable").catch(() => {}));
18456
18456
  await progress.run("enabling page events", () => client.send("Page.enable").catch(() => {}));
18457
- await progress.run("attaching to frames", () => client.send("Target.setAutoAttach", {
18458
- autoAttach: true,
18459
- waitForDebuggerOnStart: false,
18460
- flatten: true,
18461
- filter: [{
18462
- type: "worker",
18463
- exclude: true
18464
- }, {
18465
- type: "shared_worker",
18466
- exclude: true
18467
- }]
18468
- }).catch(() => {}));
18469
18457
  progress.throwIfStopped();
18470
18458
  await new Promise((resolve) => {
18471
18459
  const inflight = /* @__PURE__ */ new Set();
@@ -66131,7 +66119,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
66131
66119
  };
66132
66120
  var package_default = {
66133
66121
  name: "@browserbasehq/stagehand-extension",
66134
- version: "1.3.0-alpha-1e50db680f0429b3fb96e0519cb6d9de8c4640a5",
66122
+ version: "1.3.0-alpha-c82e6170c29ab6fb17c4a10e44812bd29507e7d4",
66135
66123
  "private": true,
66136
66124
  type: "module",
66137
66125
  scripts: {
@@ -70124,6 +70112,7 @@ var StagehandRuntime = class {
70124
70112
  this.state = createStore()(() => StagehandRuntimeStateSchema.parse({ status: "idle" }));
70125
70113
  this.pagesById = /* @__PURE__ */ new Map();
70126
70114
  this.pageEventSubscriptions = /* @__PURE__ */ new Map();
70115
+ this.pendingBrowserSessionCleanup = /* @__PURE__ */ new Map();
70127
70116
  this.browserConnectionGeneration = 0;
70128
70117
  this.initializationInProgress = false;
70129
70118
  this.lifecycleTail = Promise.resolve();
@@ -70134,39 +70123,49 @@ var StagehandRuntime = class {
70134
70123
  async replaceBrowserConnection(params, bootstrapLogger) {
70135
70124
  const { cdpUrl } = params;
70136
70125
  const generation = ++this.browserConnectionGeneration;
70137
- const previousSession = this.browserSession;
70126
+ if (this.browserSession && !this.pendingBrowserSessionCleanup.has(this.browserSession)) this.pendingBrowserSessionCleanup.set(this.browserSession, void 0);
70138
70127
  this.browserSession = void 0;
70139
70128
  this.browserSessionCdpUrl = void 0;
70140
70129
  this.disposeAllPageEventSubscriptions();
70141
70130
  this.pagesById.clear();
70142
70131
  this.responseHandles.clear();
70143
- await previousSession?.close();
70144
- try {
70145
- const session = await this.adapters.browserSessionFactory(cdpUrl, this.logger, bootstrapLogger);
70146
- if (generation !== this.browserConnectionGeneration) {
70147
- await session.close();
70148
- throw new Error("Stagehand browser connection was dropped while connecting");
70149
- }
70150
- this.browserSession = session;
70151
- this.browserSessionCdpUrl = cdpUrl;
70152
- } catch (error) {
70153
- if (generation === this.browserConnectionGeneration) {
70154
- await this.browserSession?.close();
70155
- this.browserSession = void 0;
70156
- }
70157
- throw error;
70132
+ await this.runPendingSessionCleanup();
70133
+ if (generation !== this.browserConnectionGeneration) throw new Error("Stagehand browser connection was dropped while connecting");
70134
+ const session = await this.adapters.browserSessionFactory(cdpUrl, this.logger, bootstrapLogger);
70135
+ if (generation !== this.browserConnectionGeneration) {
70136
+ await this.cleanupSession(session);
70137
+ throw new Error("Stagehand browser connection was dropped while connecting");
70158
70138
  }
70139
+ this.browserSession = session;
70140
+ this.browserSessionCdpUrl = cdpUrl;
70159
70141
  }
70160
70142
  /**
70161
70143
  * Closes the runtime's own browser connection immediately, outside the lifecycle
70162
70144
  * queue: a lost client must not wait behind lifecycle work that needs that client.
70163
70145
  */
70164
- async dropBrowserConnection() {
70146
+ async disconnectBrowser() {
70165
70147
  this.browserConnectionGeneration += 1;
70166
- const session = this.browserSession;
70148
+ if (this.browserSession && !this.pendingBrowserSessionCleanup.has(this.browserSession)) this.pendingBrowserSessionCleanup.set(this.browserSession, void 0);
70167
70149
  this.browserSession = void 0;
70168
70150
  this.browserSessionCdpUrl = void 0;
70169
- await session?.close();
70151
+ await this.runPendingSessionCleanup();
70152
+ }
70153
+ cleanupSession(session) {
70154
+ const pending = this.pendingBrowserSessionCleanup.get(session);
70155
+ if (pending) return pending;
70156
+ const closing = Promise.resolve().then(() => session.close()).then(() => {
70157
+ this.pendingBrowserSessionCleanup.delete(session);
70158
+ }, (error) => {
70159
+ this.pendingBrowserSessionCleanup.set(session, void 0);
70160
+ throw error;
70161
+ });
70162
+ this.pendingBrowserSessionCleanup.set(session, closing);
70163
+ return closing;
70164
+ }
70165
+ async runPendingSessionCleanup() {
70166
+ const errors = (await Promise.allSettled(Array.from(this.pendingBrowserSessionCleanup.keys(), (session) => this.cleanupSession(session)))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
70167
+ if (errors.length === 1) throw errors[0];
70168
+ if (errors.length > 1) throw new AggregateError(errors, "Browser session cleanup failed");
70170
70169
  }
70171
70170
  async ensureBrowserConnection(cdpUrl) {
70172
70171
  if (!this.browserSession?.connected || this.browserSessionCdpUrl !== cdpUrl) await this.replaceBrowserConnection({ cdpUrl });
@@ -70582,11 +70581,9 @@ var StagehandRuntime = class {
70582
70581
  }
70583
70582
  async close() {
70584
70583
  await this.enqueueLifecycle(async () => {
70585
- const session = this.browserSession;
70586
- this.browserSession = void 0;
70587
- this.browserSessionCdpUrl = void 0;
70588
70584
  this.clearStagehandInstance();
70589
- await session?.close();
70585
+ await this.disconnectBrowser();
70586
+ if (!this.stagehandInstanceDisposal) this.stagehandInstanceClosing = false;
70590
70587
  });
70591
70588
  }
70592
70589
  async disposeStagehandInstance() {
@@ -70595,10 +70592,13 @@ var StagehandRuntime = class {
70595
70592
  const disposal = this.enqueueLifecycle(async () => {
70596
70593
  this.disposeAllPageEventSubscriptions();
70597
70594
  await this.waitForStagehandInstanceRequests();
70595
+ const session = this.browserSession;
70596
+ if (session?.getDomainPolicy()) await session.setDomainPolicy(null);
70597
+ await this.runPendingSessionCleanup();
70598
70598
  this.clearStagehandInstance();
70599
+ this.stagehandInstanceClosing = false;
70599
70600
  });
70600
70601
  this.stagehandInstanceDisposal = disposal.finally(() => {
70601
- this.stagehandInstanceClosing = false;
70602
70602
  this.stagehandInstanceDisposal = void 0;
70603
70603
  });
70604
70604
  return await this.stagehandInstanceDisposal;
@@ -71042,7 +71042,6 @@ function cookieMatchesFilter(cookie, options) {
71042
71042
  //#endregion
71043
71043
  //#region understudy/domainPolicy.ts
71044
71044
  var DOMAIN_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
71045
- var HTTP_SCHEMES = ["http", "https"];
71046
71045
  function canonicalizeHostname(hostname) {
71047
71046
  return hostname.toLowerCase().replace(/\.+$/, "");
71048
71047
  }
@@ -71095,30 +71094,6 @@ function getDomainPatterns(policy, key) {
71095
71094
  function patternHost(rule) {
71096
71095
  return rule.type === "wildcard" ? `*.${rule.hostname}` : rule.hostname;
71097
71096
  }
71098
- function fetchPatternHosts(rule) {
71099
- const host = patternHost(rule);
71100
- return [host, `${host}.`];
71101
- }
71102
- function toBlocklistFetchPatterns(rules) {
71103
- const patterns = [];
71104
- for (const rule of rules) for (const host of fetchPatternHosts(rule)) for (const scheme of HTTP_SCHEMES) {
71105
- patterns.push({
71106
- urlPattern: `${scheme}://${host}/*`,
71107
- requestStage: "Request"
71108
- });
71109
- patterns.push({
71110
- urlPattern: `${scheme}://${host}:*/*`,
71111
- requestStage: "Request"
71112
- });
71113
- }
71114
- return patterns;
71115
- }
71116
- function toAllowlistFetchPatterns() {
71117
- return HTTP_SCHEMES.map((scheme) => ({
71118
- urlPattern: `${scheme}://*/*`,
71119
- requestStage: "Request"
71120
- }));
71121
- }
71122
71097
  function normalizeDomainPolicy(policy) {
71123
71098
  const allowedDomains = getDomainPatterns(policy, "allowedDomains");
71124
71099
  const blockedDomains = getDomainPatterns(policy, "blockedDomains");
@@ -71129,8 +71104,7 @@ function normalizeDomainPolicy(policy) {
71129
71104
  allowedDomains: allowedDomainRules.map(patternHost),
71130
71105
  blockedDomains: blockedDomainRules.map(patternHost),
71131
71106
  allowedDomainRules,
71132
- blockedDomainRules,
71133
- fetchPatterns: allowedDomainRules.length ? toAllowlistFetchPatterns() : toBlocklistFetchPatterns(blockedDomainRules)
71107
+ blockedDomainRules
71134
71108
  };
71135
71109
  }
71136
71110
  function hostnameFromHttpUrl(url) {
@@ -71162,6 +71136,53 @@ function getDomainPolicyDecision(url, policy) {
71162
71136
  };
71163
71137
  return { action: "continue" };
71164
71138
  }
71139
+ /**
71140
+ * Compile a normalized policy into browser-wide DNR session rules without installing them.
71141
+ * IDs are deterministic within this rule set; the installer must own their namespace.
71142
+ * Chrome rule limits and regex support must be checked by the installer before activation.
71143
+ */
71144
+ function toDomainPolicyDnrRules(policy) {
71145
+ if (!policy) return [];
71146
+ const rules = [];
71147
+ const addRule = (type, priority, regexFilter) => {
71148
+ rules.push({
71149
+ id: rules.length + 1,
71150
+ priority,
71151
+ action: { type },
71152
+ condition: {
71153
+ regexFilter,
71154
+ isUrlFilterCaseSensitive: false,
71155
+ resourceTypes: [
71156
+ "main_frame",
71157
+ "sub_frame",
71158
+ "stylesheet",
71159
+ "script",
71160
+ "image",
71161
+ "font",
71162
+ "object",
71163
+ "xmlhttprequest",
71164
+ "ping",
71165
+ "csp_report",
71166
+ "media",
71167
+ "websocket",
71168
+ "webtransport",
71169
+ "webbundle",
71170
+ "other"
71171
+ ]
71172
+ }
71173
+ });
71174
+ };
71175
+ if (policy.allowedDomainRules.length) {
71176
+ addRule("block", 1, "^https?://");
71177
+ for (const rule of policy.allowedDomainRules) addRule("allow", 2, domainRuleDnrRegex(rule));
71178
+ }
71179
+ for (const rule of policy.blockedDomainRules) addRule("block", 3, domainRuleDnrRegex(rule));
71180
+ return rules;
71181
+ }
71182
+ function domainRuleDnrRegex(rule) {
71183
+ const hostname = rule.hostname.replace(/\./g, "\\.");
71184
+ return `^https?://([^/?#@]*@)?${rule.type === "wildcard" ? "[^/?#@:]*\\." : ""}${hostname}\\.*(:[0-9]+)?(/|$)`;
71185
+ }
71165
71186
  var RequestSchema = discriminatedUnion("action", [strictObject({
71166
71187
  action: literal("attach"),
71167
71188
  targetId: string().min(1),
@@ -71293,7 +71314,7 @@ var DEFAULT_ACTIVE_PAGE_TIMEOUT_MS = 3e3;
71293
71314
  * so Page can record the correct owner at event time.
71294
71315
  */
71295
71316
  var BrowserContext = class BrowserContext {
71296
- constructor(conn, logger, chromeTabs, env = "LOCAL", localBrowserLaunchOptions = null, blankPageUrl = "about:blank", fallbackLocatorScriptSource = null) {
71317
+ constructor(conn, logger, chromeTabs, env = "LOCAL", localBrowserLaunchOptions = null, blankPageUrl = "about:blank", fallbackLocatorScriptSource = null, domainPolicyRules) {
71297
71318
  this.conn = conn;
71298
71319
  this.logger = logger;
71299
71320
  this.chromeTabs = chromeTabs;
@@ -71301,8 +71322,8 @@ var BrowserContext = class BrowserContext {
71301
71322
  this.localBrowserLaunchOptions = localBrowserLaunchOptions;
71302
71323
  this.blankPageUrl = blankPageUrl;
71303
71324
  this.fallbackLocatorScriptSource = fallbackLocatorScriptSource;
71325
+ this.domainPolicyRules = domainPolicyRules;
71304
71326
  this._targetSessionListeners = /* @__PURE__ */ new Set();
71305
- this._domainPolicySessionListeners = /* @__PURE__ */ new Map();
71306
71327
  this._sessionInit = /* @__PURE__ */ new Set();
71307
71328
  this.pagesByTarget = /* @__PURE__ */ new Map();
71308
71329
  this.mainFrameToTarget = /* @__PURE__ */ new Map();
@@ -71322,6 +71343,8 @@ var BrowserContext = class BrowserContext {
71322
71343
  this.initScripts = [];
71323
71344
  this.extraHttpHeaders = null;
71324
71345
  this.domainPolicy = null;
71346
+ this.domainPolicyTail = Promise.resolve();
71347
+ this.closing = false;
71325
71348
  }
71326
71349
  get connected() {
71327
71350
  return this.conn.connected;
@@ -71350,13 +71373,22 @@ var BrowserContext = class BrowserContext {
71350
71373
  static async create(wsUrl, opts) {
71351
71374
  const connectTask = async () => {
71352
71375
  const conn = await CdpConnection.connect(wsUrl, opts.websocketFactory, opts.logger);
71353
- const ctx = new BrowserContext(conn, opts.logger, opts.chromeTabs, opts?.env ?? "LOCAL", opts?.localBrowserLaunchOptions ?? null, opts.blankPageUrl, opts.fallbackLocatorScriptSource);
71376
+ const ctx = new BrowserContext(conn, opts.logger, opts.chromeTabs, opts?.env ?? "LOCAL", opts?.localBrowserLaunchOptions ?? null, opts.blankPageUrl, opts.fallbackLocatorScriptSource, opts.domainPolicyRules);
71354
71377
  const bootstrap = async () => {
71355
71378
  await ctx.bootstrap();
71356
71379
  if (!ctx.hasTopLevelPage()) await ctx.newPage();
71357
71380
  };
71358
- if (opts.bootstrapLogger) await conn.runWithTelemetryContext(Symbol("browser.bootstrap"), opts.bootstrapLogger, bootstrap);
71359
- else await bootstrap();
71381
+ try {
71382
+ if (opts.bootstrapLogger) await conn.runWithTelemetryContext(Symbol("browser.bootstrap"), opts.bootstrapLogger, bootstrap);
71383
+ else await bootstrap();
71384
+ } catch (error) {
71385
+ try {
71386
+ await ctx.close();
71387
+ } catch (cleanupError) {
71388
+ throw new AggregateError([error, cleanupError], "Browser context initialization and cleanup failed");
71389
+ }
71390
+ throw error;
71391
+ }
71360
71392
  return ctx;
71361
71393
  };
71362
71394
  return await connectTask();
@@ -71456,96 +71488,16 @@ var BrowserContext = class BrowserContext {
71456
71488
  }
71457
71489
  async setDomainPolicy(policy) {
71458
71490
  const nextPolicy = normalizeDomainPolicy(policy);
71459
- this.domainPolicy = nextPolicy;
71460
- const sessions = [];
71461
- for (const sessionId of this._sessionInit) {
71462
- const session = this.conn.getSession(sessionId);
71463
- if (session) sessions.push(session);
71464
- }
71465
- if (!sessions.length) return;
71466
- const failures = (await Promise.allSettled(sessions.map(async (session) => {
71467
- if (!nextPolicy) {
71468
- try {
71469
- await session.send("Fetch.disable");
71470
- this.uninstallDomainPolicyHandler(session);
71471
- } catch (error) {
71472
- throw {
71473
- action: "disable",
71474
- error
71475
- };
71476
- }
71477
- return;
71478
- }
71479
- this.installDomainPolicyHandler(session);
71480
- try {
71481
- await session.send("Fetch.enable", { patterns: nextPolicy.fetchPatterns });
71482
- } catch (error) {
71483
- throw {
71484
- action: "enable",
71485
- error
71486
- };
71487
- }
71488
- }))).map((result, index) => ({
71489
- result,
71490
- session: sessions[index]
71491
- })).filter((entry) => entry.result.status === "rejected").map((entry) => {
71492
- const failure = entry.result.reason;
71493
- if (failure?.action === "enable") this.uninstallDomainPolicyHandler(entry.session);
71494
- const reason = failure?.error ?? entry.result.reason;
71495
- return {
71496
- reason,
71497
- sessionId: entry.session.id ?? "unknown",
71498
- message: reason instanceof Error ? reason.message : String(reason)
71499
- };
71491
+ if (this.closing) throw new Error("Browser context is closing");
71492
+ const rules = this.domainPolicyRules;
71493
+ if (!rules) throw new Error("Domain policy DNR enforcement is unavailable");
71494
+ const update = this.domainPolicyTail.then(async () => {
71495
+ if (this.closing) throw new Error("Browser context is closing");
71496
+ await rules.replace(nextPolicy);
71497
+ this.domainPolicy = nextPolicy;
71500
71498
  });
71501
- if (failures.length) {
71502
- this.logger.error("setDomainPolicy failed for one or more sessions", {
71503
- category: "ctx",
71504
- failures: failures.map(({ sessionId, message }) => ({
71505
- sessionId,
71506
- message
71507
- }))
71508
- });
71509
- throw failures[0].reason;
71510
- }
71511
- }
71512
- installDomainPolicyHandler(session) {
71513
- const sessionId = session.id;
71514
- if (!sessionId) return;
71515
- if (this._domainPolicySessionListeners.has(sessionId)) return;
71516
- const handler = (evt) => {
71517
- this.handleDomainPolicyRequestPaused(session, evt);
71518
- };
71519
- this._domainPolicySessionListeners.set(sessionId, handler);
71520
- session.on("Fetch.requestPaused", handler);
71521
- }
71522
- uninstallDomainPolicyHandler(session) {
71523
- const sessionId = session.id;
71524
- if (!sessionId) return;
71525
- const handler = this._domainPolicySessionListeners.get(sessionId);
71526
- if (!handler) return;
71527
- session.off("Fetch.requestPaused", handler);
71528
- this._domainPolicySessionListeners.delete(sessionId);
71529
- }
71530
- async handleDomainPolicyRequestPaused(session, evt) {
71531
- const decision = getDomainPolicyDecision(evt.request.url, this.domainPolicy);
71532
- if (decision.action === "continue") {
71533
- await session.send("Fetch.continueRequest", { requestId: evt.requestId }).catch(() => {});
71534
- return;
71535
- }
71536
- let hostname = "";
71537
- try {
71538
- hostname = new URL(evt.request.url).hostname.toLowerCase();
71539
- } catch {}
71540
- this.logger.debug("Blocked request by domain policy", {
71541
- category: "network",
71542
- hostname,
71543
- ruleType: decision.reason
71544
- });
71545
- await session.send("Fetch.failRequest", {
71546
- requestId: evt.requestId,
71547
- errorReason: "BlockedByClient"
71548
- }).catch(() => {});
71499
+ this.domainPolicyTail = update.catch(() => void 0);
71500
+ await update;
71549
71501
  }
71550
71502
  get clipboard() {
71551
71503
  return this._clipboard ??= new ContextClipboard({
@@ -71629,23 +71581,42 @@ var BrowserContext = class BrowserContext {
71629
71581
  /**
71630
71582
  * Close CDP and clear all mappings. Best-effort cleanup.
71631
71583
  */
71632
- async close() {
71633
- await this.conn.close();
71634
- this.pagesByTarget.clear();
71635
- this.mainFrameToTarget.clear();
71636
- this.sessionOwnerPage.clear();
71637
- this.frameOwnerPage.clear();
71638
- this.pendingOopifByMainFrame.clear();
71639
- this.pendingOopifParentByMainFrame.clear();
71640
- this.adoptedOopifSessionByFrame.clear();
71641
- this.createdAtByTarget.clear();
71642
- this.typeByTarget.clear();
71643
- this.pendingCreatedTargetUrl.clear();
71644
- this.pageCreationFailures.clear();
71645
- this.pendingInitialTopLevelTargets.clear();
71646
- this.pendingNewPageTargets.clear();
71647
- this.domainPolicyClosingTargets.clear();
71648
- this.domainPolicyClosePromises.clear();
71584
+ close() {
71585
+ if (this.closePromise) return this.closePromise;
71586
+ this.closing = true;
71587
+ const closing = Promise.resolve().then(async () => {
71588
+ try {
71589
+ await this.conn.close();
71590
+ } finally {
71591
+ await this.domainPolicyTail;
71592
+ try {
71593
+ await this.domainPolicyRules?.close();
71594
+ this.domainPolicy = null;
71595
+ } finally {
71596
+ this.pagesByTarget.clear();
71597
+ this.mainFrameToTarget.clear();
71598
+ this.sessionOwnerPage.clear();
71599
+ this.frameOwnerPage.clear();
71600
+ this.pendingOopifByMainFrame.clear();
71601
+ this.pendingOopifParentByMainFrame.clear();
71602
+ this.adoptedOopifSessionByFrame.clear();
71603
+ this.createdAtByTarget.clear();
71604
+ this.typeByTarget.clear();
71605
+ this.pendingCreatedTargetUrl.clear();
71606
+ this.pageCreationFailures.clear();
71607
+ this.pendingInitialTopLevelTargets.clear();
71608
+ this.pendingNewPageTargets.clear();
71609
+ this.domainPolicyClosingTargets.clear();
71610
+ this.domainPolicyClosePromises.clear();
71611
+ this._sessionInit.clear();
71612
+ }
71613
+ }
71614
+ });
71615
+ this.closePromise = closing.catch((error) => {
71616
+ this.closePromise = void 0;
71617
+ throw error;
71618
+ });
71619
+ return this.closePromise;
71649
71620
  }
71650
71621
  /**
71651
71622
  * Bootstrap target lifecycle:
@@ -71724,20 +71695,6 @@ var BrowserContext = class BrowserContext {
71724
71695
  response: session.send(method, params).then(() => true).catch(() => false)
71725
71696
  };
71726
71697
  };
71727
- const queueFetchEnablePreResume = (params) => {
71728
- let error;
71729
- return {
71730
- dispatched: this.conn.waitForSessionDispatch(sessionId, "Fetch.enable").then(() => true).catch((err) => {
71731
- error = err;
71732
- return false;
71733
- }),
71734
- response: session.send("Fetch.enable", params).then(() => true).catch((err) => {
71735
- error = err;
71736
- return false;
71737
- }),
71738
- getError: () => error
71739
- };
71740
- };
71741
71698
  const initScriptOps = [];
71742
71699
  const corePreResumeOps = [
71743
71700
  queuePreResume("Page.enable"),
@@ -71754,11 +71711,6 @@ var BrowserContext = class BrowserContext {
71754
71711
  headerPreResumeOps.push(queuePreResume("Network.enable"));
71755
71712
  headerPreResumeOps.push(queuePreResume("Network.setExtraHTTPHeaders", { headers }));
71756
71713
  }
71757
- const fetchPreResumeOps = [];
71758
- if (this.domainPolicy) {
71759
- this.installDomainPolicyHandler(session);
71760
- fetchPreResumeOps.push(queueFetchEnablePreResume({ patterns: this.domainPolicy.fetchPatterns }));
71761
- }
71762
71714
  if (this.initScripts.length) for (const source of this.initScripts) initScriptOps.push(queuePreResume("Page.addScriptToEvaluateOnNewDocument", {
71763
71715
  source,
71764
71716
  runImmediately: true
@@ -71766,15 +71718,13 @@ var BrowserContext = class BrowserContext {
71766
71718
  const preResumeDispatched = (await Promise.all([
71767
71719
  ...corePreResumeOps.map((op) => op.dispatched),
71768
71720
  ...headerPreResumeOps.map((op) => op.dispatched),
71769
- ...fetchPreResumeOps.map((op) => op.dispatched),
71770
71721
  ...initScriptOps.map((op) => op.dispatched)
71771
71722
  ])).every(Boolean);
71772
71723
  const resumeOp = queuePreResume("Runtime.runIfWaitingForDebugger");
71773
71724
  const [resumedDispatched, resumedOk] = await Promise.all([resumeOp.dispatched, resumeOp.response]);
71774
- const [coreResults, headerResults, fetchResults, initScriptResults] = await Promise.all([
71725
+ const [coreResults, headerResults, initScriptResults] = await Promise.all([
71775
71726
  Promise.all(corePreResumeOps.map((op) => op.response)),
71776
71727
  Promise.all(headerPreResumeOps.map((op) => op.response)),
71777
- Promise.all(fetchPreResumeOps.map((op) => op.response)),
71778
71728
  Promise.all(initScriptOps.map((op) => op.response))
71779
71729
  ]);
71780
71730
  if (!preResumeDispatched || !resumedDispatched || !resumedOk) {
@@ -71792,23 +71742,6 @@ var BrowserContext = class BrowserContext {
71792
71742
  return;
71793
71743
  }
71794
71744
  resumed = true;
71795
- if (fetchPreResumeOps.length > 0 && !fetchResults.every(Boolean)) {
71796
- this.uninstallDomainPolicyHandler(session);
71797
- const fetchError = fetchPreResumeOps.map((op) => op.getError()).find((error) => error !== void 0);
71798
- const fetchErrorMessage = fetchError ? fetchError instanceof Error ? fetchError.message : String(fetchError) : "Fetch.enable failed during target attach";
71799
- const policyFailureMessage = "Fetch.enable failed during target attach; closing target because Stagehand cannot guarantee domain policy enforcement";
71800
- this.recordPageCreationFailure(info.targetId, fetchError instanceof Error ? fetchError : /* @__PURE__ */ new Error(`${policyFailureMessage}: ${fetchErrorMessage}`), policyFailureMessage);
71801
- this.logger.error("Closing target because domain policy could not be guaranteed", {
71802
- category: "ctx",
71803
- targetId: String(info.targetId),
71804
- targetType: String(info.type),
71805
- targetUrl: String(info.url ?? ""),
71806
- sessionId,
71807
- cdpError: fetchErrorMessage
71808
- });
71809
- await this.conn.send("Target.closeTarget", { targetId: info.targetId }).catch(() => {});
71810
- return;
71811
- }
71812
71745
  const scriptsInstalled = coreResults.every(Boolean) && initScriptResults.every(Boolean);
71813
71746
  try {
71814
71747
  session.send("Page.setLifecycleEventsEnabled", { enabled: true }).catch(() => {});
@@ -71935,7 +71868,7 @@ var BrowserContext = class BrowserContext {
71935
71868
  if (existingClose) return source === "attached" ? await existingClose : true;
71936
71869
  const decision = getDomainPolicyDecision(info.url ?? "", this.domainPolicy);
71937
71870
  if (decision.action === "continue") return false;
71938
- this.logger.debug("Popup reached a disallowed domain before it could be intercepted; closing it", {
71871
+ this.logger.debug("Closing popup targeting a disallowed domain", {
71939
71872
  category: "network",
71940
71873
  targetId: String(info.targetId),
71941
71874
  targetUrl: String(info.url ?? ""),
@@ -71986,9 +71919,6 @@ var BrowserContext = class BrowserContext {
71986
71919
  for (const [fid, sid] of Array.from(this.pendingOopifByMainFrame.entries())) if (sid === sessionId) this.removePendingOopif(fid);
71987
71920
  for (const [fid, sid] of Array.from(this.adoptedOopifSessionByFrame.entries())) if (sid === sessionId) this.adoptedOopifSessionByFrame.delete(fid);
71988
71921
  this._targetSessionListeners.delete(sessionId);
71989
- const session = this.conn.getSession(sessionId);
71990
- if (session) this.uninstallDomainPolicyHandler(session);
71991
- else this._domainPolicySessionListeners.delete(sessionId);
71992
71922
  this._sessionInit.delete(sessionId);
71993
71923
  }
71994
71924
  /**
@@ -72122,6 +72052,95 @@ var BrowserContext = class BrowserContext {
72122
72052
  }
72123
72053
  };
72124
72054
  //#endregion
72055
+ //#region understudy/domainPolicyDnr.ts
72056
+ var DOMAIN_POLICY_RULE_ID_START = 1e6;
72057
+ var DOMAIN_POLICY_RULE_ID_END = 101e4;
72058
+ function isDomainPolicyRule(rule) {
72059
+ return rule.id >= 1e6 && rule.id < DOMAIN_POLICY_RULE_ID_END;
72060
+ }
72061
+ /** One manager per extension, with one active browser session at a time. */
72062
+ var DomainPolicyDnrManager = class {
72063
+ constructor(api, extensionId) {
72064
+ this.api = api;
72065
+ this.extensionId = extensionId;
72066
+ this.tail = Promise.resolve();
72067
+ }
72068
+ openSession() {
72069
+ return this.enqueue(async () => {
72070
+ await this.replaceRules([]);
72071
+ const owner = Symbol("domain-policy");
72072
+ this.owner = owner;
72073
+ return {
72074
+ replace: (policy) => {
72075
+ const rules = this.compile(policy);
72076
+ return this.enqueue(async () => {
72077
+ if (this.owner !== owner) throw new Error("Domain policy session is no longer active");
72078
+ await this.replaceRules(rules);
72079
+ });
72080
+ },
72081
+ close: () => this.enqueue(async () => {
72082
+ if (this.owner !== owner) return;
72083
+ await this.replaceRules([]);
72084
+ this.owner = void 0;
72085
+ })
72086
+ };
72087
+ });
72088
+ }
72089
+ compile(policy) {
72090
+ const rules = toDomainPolicyDnrRules(policy).map((rule) => ({
72091
+ ...rule,
72092
+ id: DOMAIN_POLICY_RULE_ID_START + rule.id
72093
+ }));
72094
+ if (!rules.length) return rules;
72095
+ rules.unshift({
72096
+ id: DOMAIN_POLICY_RULE_ID_START,
72097
+ priority: 4,
72098
+ action: { type: "allow" },
72099
+ condition: {
72100
+ regexFilter: "^https?://",
72101
+ isUrlFilterCaseSensitive: false,
72102
+ excludedResourceTypes: [],
72103
+ initiatorDomains: [this.extensionId],
72104
+ tabIds: [-1]
72105
+ }
72106
+ });
72107
+ return rules;
72108
+ }
72109
+ async replaceRules(rules) {
72110
+ const existing = await this.api.getSessionRules();
72111
+ const owned = existing.filter(isDomainPolicyRule);
72112
+ const unrelated = existing.filter((rule) => !isDomainPolicyRule(rule));
72113
+ if (rules.length) {
72114
+ if (rules.some((rule) => !isDomainPolicyRule(rule)) || unrelated.length + rules.length > this.api.MAX_NUMBER_OF_SESSION_RULES) throw new Error("Domain policy exceeds Chrome's session rule limit");
72115
+ if ([...unrelated, ...rules].filter((rule) => rule.condition.regexFilter !== void 0).length > this.api.MAX_NUMBER_OF_REGEX_RULES) throw new Error("Domain policy exceeds Chrome's regex rule limit");
72116
+ const regexes = /* @__PURE__ */ new Map();
72117
+ for (const { condition } of rules) {
72118
+ if (condition.regexFilter === void 0) continue;
72119
+ const options = {
72120
+ regex: condition.regexFilter,
72121
+ isCaseSensitive: condition.isUrlFilterCaseSensitive ?? false,
72122
+ requireCapturing: false
72123
+ };
72124
+ regexes.set(JSON.stringify(options), options);
72125
+ }
72126
+ await Promise.all(Array.from(regexes.values(), async (options) => {
72127
+ const result = await this.api.isRegexSupported(options);
72128
+ if (!result.isSupported) throw new Error("Chrome does not support a domain policy regex: " + (result.reason ?? "unknown reason"));
72129
+ }));
72130
+ }
72131
+ if (!owned.length && !rules.length) return;
72132
+ await this.api.updateSessionRules({
72133
+ removeRuleIds: owned.map((rule) => rule.id),
72134
+ addRules: rules
72135
+ });
72136
+ }
72137
+ enqueue(work) {
72138
+ const result = this.tail.then(work, work);
72139
+ this.tail = result.catch(() => void 0);
72140
+ return result;
72141
+ }
72142
+ };
72143
+ //#endregion
72125
72144
  //#region version.ts
72126
72145
  var STAGEHAND_RUNTIME_VERSION = package_default.version;
72127
72146
  //#endregion
@@ -72129,18 +72148,31 @@ var STAGEHAND_RUNTIME_VERSION = package_default.version;
72129
72148
  function startStagehandServiceWorker(scope = globalThis, runtime) {
72130
72149
  const chromeRuntimeClient = new ChromeRuntimeClient(scope, STAGEHAND_SEND_TO_HOST_BINDING);
72131
72150
  let rpcClient;
72151
+ let domainPolicyManager;
72132
72152
  const activeRuntime = runtime ?? createStagehandRuntime({
72133
72153
  browserSessionFactory: async (cdpUrl, logger, bootstrapLogger) => {
72134
- const locatorRuntimeResponse = await fetch(chrome.runtime.getURL("content-script.js"));
72135
- if (!locatorRuntimeResponse.ok) throw new Error(`Failed to load Stagehand locator runtime: ${locatorRuntimeResponse.status}`);
72136
- return BrowserContext.create(cdpUrl, {
72137
- websocketFactory: browserWebSocketFactory,
72138
- logger,
72139
- blankPageUrl: chrome.runtime.getURL("blank.html"),
72140
- fallbackLocatorScriptSource: await locatorRuntimeResponse.text(),
72141
- chromeTabs: new ChromeTabTargetAdapter(chrome),
72142
- bootstrapLogger
72143
- });
72154
+ domainPolicyManager ??= new DomainPolicyDnrManager(chrome.declarativeNetRequest, chrome.runtime.id);
72155
+ const domainPolicyRules = await domainPolicyManager.openSession();
72156
+ try {
72157
+ const locatorRuntimeResponse = await fetch(chrome.runtime.getURL("content-script.js"));
72158
+ if (!locatorRuntimeResponse.ok) throw new Error(`Failed to load Stagehand locator runtime: ${locatorRuntimeResponse.status}`);
72159
+ return await BrowserContext.create(cdpUrl, {
72160
+ websocketFactory: browserWebSocketFactory,
72161
+ logger,
72162
+ blankPageUrl: chrome.runtime.getURL("blank.html"),
72163
+ fallbackLocatorScriptSource: await locatorRuntimeResponse.text(),
72164
+ chromeTabs: new ChromeTabTargetAdapter(chrome),
72165
+ bootstrapLogger,
72166
+ domainPolicyRules
72167
+ });
72168
+ } catch (error) {
72169
+ try {
72170
+ await domainPolicyRules.close();
72171
+ } catch (cleanupError) {
72172
+ throw new AggregateError([error, cleanupError], "Browser session initialization and domain policy cleanup failed");
72173
+ }
72174
+ throw error;
72175
+ }
72144
72176
  },
72145
72177
  emitLog: (log) => {
72146
72178
  rpcClient?.notify(StagehandNotifications.log, log).catch((error) => {
@@ -72175,7 +72207,7 @@ function startStagehandServiceWorker(scope = globalThis, runtime) {
72175
72207
  return session.conn;
72176
72208
  },
72177
72209
  rejectClientCallbacks: () => rpcClient?.releaseClient(),
72178
- closeConnection: () => activeRuntime.dropBrowserConnection(),
72210
+ closeConnection: () => activeRuntime.disconnectBrowser(),
72179
72211
  disposeClient: () => activeRuntime.disposeStagehandInstance(),
72180
72212
  onError: (error) => {
72181
72213
  console.error("Stagehand client lifecycle cleanup failed", error);
@@ -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.1.1-alpha-1e50db680f0429b3fb96e0519cb6d9de8c4640a5'
4
+ STAGEHAND_PROTOCOL_VERSION = '3.1.1-alpha-c82e6170c29ab6fb17c4a10e44812bd29507e7d4'