smooth-operator-mcp 3.0.4 → 3.0.6

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.
@@ -354,7 +354,7 @@ function truncateUtf8(value, maxBytes) {
354
354
  while (low < high) {
355
355
  const midpoint = Math.ceil((low + high) / 2);
356
356
  const candidate = decoder.decode(bytes.slice(0, midpoint));
357
- if (UTF8_ENCODER.encode(candidate).byteLength <= maxBytes) {
357
+ if (UTF8_ENCODER.encode(candidate).byteLength <= boundedMaxBytes) {
358
358
  low = midpoint;
359
359
  } else {
360
360
  high = midpoint - 1;
@@ -397,7 +397,7 @@ var SERVER_VERSION;
397
397
  var init_version = __esm({
398
398
  "src/server/version.ts"() {
399
399
  "use strict";
400
- SERVER_VERSION = "3.0.4";
400
+ SERVER_VERSION = "3.0.6";
401
401
  }
402
402
  });
403
403
 
@@ -406,17 +406,39 @@ var discovery_exports = {};
406
406
  __export(discovery_exports, {
407
407
  chromeExecutableSearchPaths: () => chromeExecutableSearchPaths,
408
408
  findChromeExecutable: () => findChromeExecutable,
409
- findChromiumExecutables: () => findChromiumExecutables
409
+ findChromiumExecutables: () => findChromiumExecutables,
410
+ isExecutableReady: () => isExecutableReady
410
411
  });
411
412
  import * as nodeFs from "node:fs";
412
413
  import { homedir as homedir2 } from "node:os";
413
414
  import { delimiter, join as join3, win32 } from "node:path";
414
415
  import { env as env2 } from "node:process";
415
416
  function findChromeExecutable(fs = nodeFs) {
416
- return dedupeCandidates(chromeExecutableCandidates()).find((candidate) => fs.existsSync(candidate.path)) ?? null;
417
+ return dedupeCandidates(chromeExecutableCandidates()).find((candidate) => isExecutableReady(candidate.path, fs)) ?? null;
417
418
  }
418
419
  function findChromiumExecutables(fs = nodeFs) {
419
- return dedupeCandidates(chromeExecutableCandidates()).filter((candidate) => fs.existsSync(candidate.path));
420
+ return dedupeCandidates(chromeExecutableCandidates()).filter((candidate) => isExecutableReady(candidate.path, fs));
421
+ }
422
+ function isExecutableReady(path, fs = nodeFs, platformName = process.platform) {
423
+ if (typeof path !== "string" || path.length === 0) {
424
+ return false;
425
+ }
426
+ try {
427
+ const stats = fs.statSync(path);
428
+ if (!stats.isFile()) {
429
+ return false;
430
+ }
431
+ if (platformName === "win32") {
432
+ return true;
433
+ }
434
+ if ((stats.mode & 73) === 0) {
435
+ return false;
436
+ }
437
+ fs.accessSync(path, nodeFs.constants.X_OK);
438
+ return true;
439
+ } catch {
440
+ return false;
441
+ }
420
442
  }
421
443
  function chromeExecutableSearchPaths() {
422
444
  return dedupeCandidates(chromeExecutableCandidates()).map((candidate) => candidate.path);
@@ -510,7 +532,7 @@ var init_discovery = __esm({
510
532
  });
511
533
 
512
534
  // src/server/installer.ts
513
- import { constants as constants2, accessSync, existsSync } from "node:fs";
535
+ import { constants as constants3, accessSync, existsSync } from "node:fs";
514
536
  import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open3, rename as rename3, unlink as unlink3, writeFile } from "node:fs/promises";
515
537
  import { execFile } from "node:child_process";
516
538
  import { randomUUID as randomUUID3 } from "node:crypto";
@@ -596,7 +618,7 @@ function resolveStableNodeExecutable() {
596
618
  const candidates = platform2() === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node"];
597
619
  for (const candidate of candidates) {
598
620
  try {
599
- accessSync(candidate, constants2.X_OK);
621
+ accessSync(candidate, constants3.X_OK);
600
622
  return candidate;
601
623
  } catch {
602
624
  }
@@ -731,20 +753,20 @@ async function installJsonConfig(target, plannedPath, options, allowOpenCodeJson
731
753
  }
732
754
  }
733
755
  async function readSecureConfigFile(path) {
734
- const noFollow = typeof constants2.O_NOFOLLOW === "number" ? constants2.O_NOFOLLOW : 0;
756
+ const noFollow = typeof constants3.O_NOFOLLOW === "number" ? constants3.O_NOFOLLOW : 0;
735
757
  if (!noFollow) {
736
758
  await rejectSymlink2(path, "configuration file");
737
759
  }
738
760
  let handle;
739
761
  try {
740
- handle = await open3(path, constants2.O_RDONLY | noFollow);
762
+ handle = await open3(path, constants3.O_RDONLY | noFollow);
741
763
  } catch (error) {
742
764
  if (isMissingFile2(error)) {
743
765
  return void 0;
744
766
  }
745
767
  if (noFollow && (isErrorCode2(error, "EINVAL") || isErrorCode2(error, "ENOTSUP") || isErrorCode2(error, "EOPNOTSUPP"))) {
746
768
  await rejectSymlink2(path, "configuration file");
747
- handle = await open3(path, constants2.O_RDONLY);
769
+ handle = await open3(path, constants3.O_RDONLY);
748
770
  } else {
749
771
  if (isErrorCode2(error, "ELOOP") || isErrorCode2(error, "EFTYPE")) {
750
772
  throw new AppError("INSTALL_CONFIG_FAILED", `The configuration file '${path}' must not be a symbolic link.`);
@@ -1238,7 +1260,7 @@ __export(installer_wizard_exports, {
1238
1260
  runWizard: () => runWizard
1239
1261
  });
1240
1262
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7, parse as parse4, resolve as resolve6, win32 as win323 } from "node:path";
1241
- import { accessSync as accessSync2, constants as constants3, statSync } from "node:fs";
1263
+ import { accessSync as accessSync2, constants as constants4, statSync } from "node:fs";
1242
1264
  import { chmod as chmod3, lstat as lstat4, rename as rename4, unlink as unlink4, writeFile as writeFile2 } from "node:fs/promises";
1243
1265
  import { homedir as homedir4 } from "node:os";
1244
1266
  import { isIP as isIP3 } from "node:net";
@@ -1261,7 +1283,7 @@ function isExecutableFile(path) {
1261
1283
  return false;
1262
1284
  }
1263
1285
  if (process.platform !== "win32") {
1264
- accessSync2(path, constants3.X_OK);
1286
+ accessSync2(path, constants4.X_OK);
1265
1287
  }
1266
1288
  return true;
1267
1289
  } catch {
@@ -2294,6 +2316,7 @@ var SecurityPolicy = class _SecurityPolicy {
2294
2316
  // src/server/config.ts
2295
2317
  var TransportSchema = z.enum(["stdio", "http"]);
2296
2318
  var BrowserModeSchema = z.enum(["disabled", "connect", "launch", "managed"]);
2319
+ var BrowserIdleTimeoutSchema = z.number().int().min(0).max(864e5);
2297
2320
  var ConfigPathSchema = z.string().trim().min(1).max(4096);
2298
2321
  var DomainPatternSchema = z.string().trim().min(1).max(253).refine(isValidDomainPattern2, "Domain patterns must be exact hostnames or *.-prefixed suffixes.");
2299
2322
  var HostPatternSchema = z.string().trim().min(1).max(255).refine(isValidHostPattern, "Host allowlists must contain hostnames or bracketed IPv6 addresses without ports.");
@@ -2326,7 +2349,8 @@ var RawConfigSchema = z.object({
2326
2349
  connectTimeoutMs: z.number().int().min(1e3).max(18e4).optional(),
2327
2350
  cdpTimeoutMs: z.number().int().min(100).max(12e4).optional(),
2328
2351
  maxScreenshotBytes: z.number().int().min(1e5).max(2e7).optional(),
2329
- maxHtmlChars: z.number().int().min(1e3).max(5e5).optional()
2352
+ maxHtmlChars: z.number().int().min(1e3).max(5e5).optional(),
2353
+ idleTimeoutMs: BrowserIdleTimeoutSchema.optional()
2330
2354
  }).strict().optional(),
2331
2355
  security: z.object({
2332
2356
  allowedDomains: ConfigList(DomainPatternSchema).optional(),
@@ -2647,6 +2671,9 @@ function validateConfig(config) {
2647
2671
  if (config.browser.maxHtmlChars < 1e3 || config.browser.maxHtmlChars > 5e5) {
2648
2672
  throw new AppError("CONFIG_INVALID", "Maximum HTML characters must be between 1000 and 500000.");
2649
2673
  }
2674
+ if (!Number.isSafeInteger(config.browser.idleTimeoutMs) || config.browser.idleTimeoutMs < 0 || config.browser.idleTimeoutMs > 864e5) {
2675
+ throw new AppError("CONFIG_INVALID", "Browser idle timeout must be between 0ms and 86400000ms.");
2676
+ }
2650
2677
  validateBrowserEndpoint(config.browser.url, ["http:", "https:"], "Browser DevTools URL");
2651
2678
  validateBrowserEndpoint(config.browser.wsEndpoint, ["ws:", "wss:"], "Browser WebSocket endpoint");
2652
2679
  if (config.stealth && config.stealth.profile !== "balanced" && config.stealth.profile !== "max") {
@@ -2729,7 +2756,8 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
2729
2756
  connectTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_CONNECT_TIMEOUT_MS, nestedBrowser.connectTimeoutMs ?? 3e4),
2730
2757
  cdpTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_CDP_TIMEOUT_MS, nestedBrowser.cdpTimeoutMs ?? 3e4),
2731
2758
  maxScreenshotBytes: parseInteger(environment.SMOOTH_OPERATOR_MAX_SCREENSHOT_BYTES, nestedBrowser.maxScreenshotBytes ?? 8e6),
2732
- maxHtmlChars: parseInteger(environment.SMOOTH_OPERATOR_MAX_HTML_CHARS, nestedBrowser.maxHtmlChars ?? 2e5)
2759
+ maxHtmlChars: parseInteger(environment.SMOOTH_OPERATOR_MAX_HTML_CHARS, nestedBrowser.maxHtmlChars ?? 2e5),
2760
+ idleTimeoutMs: parseInteger(environment.SMOOTH_OPERATOR_BROWSER_IDLE_TIMEOUT_MS, nestedBrowser.idleTimeoutMs ?? 0)
2733
2761
  },
2734
2762
  security: {
2735
2763
  allowedDomains: normalizeDomainList(parseList(environment.SMOOTH_OPERATOR_ALLOWED_DOMAINS, nestedSecurity.allowedDomains ?? [])),
@@ -2795,6 +2823,9 @@ var RESEARCH_QUERY_MAX_CHARS = 4e3;
2795
2823
  var RESEARCH_MIN_CHARS = 500;
2796
2824
  var RESEARCH_MAX_CHARS = 4e3;
2797
2825
  var RESEARCH_MAX_RESULTS = 10;
2826
+ var RESOURCE_BLOCKING_TYPES = ["image", "stylesheet", "font", "media", "script"];
2827
+ var ResourceBlockingTypeSchema = z2.enum(RESOURCE_BLOCKING_TYPES);
2828
+ var ResourceBlockingOperationSchema = z2.enum(["get", "set", "clear"]);
2798
2829
  var isHttpUrl = (value) => {
2799
2830
  try {
2800
2831
  const url = new URL(value);
@@ -2831,6 +2862,8 @@ var BrowserActionNames = [
2831
2862
  "enable_network_log",
2832
2863
  "disable_network_log",
2833
2864
  "get_network_log",
2865
+ "search_network_log",
2866
+ "resource_blocking",
2834
2867
  "clear_network_log",
2835
2868
  "getclear_network_log",
2836
2869
  // canonical action spelling of the read_and_clear operation
@@ -2851,6 +2884,7 @@ var BrowserActionNames = [
2851
2884
  "page_next",
2852
2885
  "search_page",
2853
2886
  "find_elements",
2887
+ "inspect_element",
2854
2888
  "list_interactive",
2855
2889
  "list_frames",
2856
2890
  "accessibility_snapshot",
@@ -2920,15 +2954,25 @@ var BrowserActionFieldsSchema = z2.object({
2920
2954
  state: z2.enum(["visible", "hidden", "attached", "detached"]).optional(),
2921
2955
  waitUntil: z2.enum(["load", "domcontentloaded", "networkidle0", "networkidle2"]).optional(),
2922
2956
  filePath: BoundedString(4e3).optional(),
2957
+ filePaths: z2.array(BoundedString(4e3)).min(1).max(20).optional(),
2923
2958
  outputPath: BoundedString(4e3).optional(),
2924
2959
  code: z2.string().trim().min(1).max(4e4).optional(),
2925
2960
  script: z2.string().trim().min(1).max(4e4).optional(),
2926
2961
  expression: z2.string().trim().min(1).max(4e4).optional(),
2927
2962
  query: BoundedString(4e3).optional(),
2963
+ requestId: BoundedString(256).optional(),
2964
+ method: BoundedString(32).optional(),
2965
+ status: z2.number().int().min(0).max(999).optional(),
2966
+ resourceType: BoundedString(64).optional(),
2967
+ operation: ResourceBlockingOperationSchema.optional(),
2968
+ resourceTypes: z2.array(ResourceBlockingTypeSchema).min(1).max(RESOURCE_BLOCKING_TYPES.length).optional(),
2969
+ limit: z2.number().int().min(1).max(200).optional(),
2928
2970
  includeLinks: z2.boolean().optional(),
2929
2971
  includeSnapshot: z2.boolean().optional(),
2930
2972
  maxChars: z2.number().int().min(100).max(MCP_PAGE_TEXT_MAX_CHARS).optional(),
2931
2973
  maxNodes: z2.number().int().min(1).max(2e3).optional(),
2974
+ maxDepth: z2.number().int().min(0).max(3).optional(),
2975
+ maxChildren: z2.number().int().min(1).max(100).optional(),
2932
2976
  interestingOnly: z2.boolean().optional(),
2933
2977
  maxBytes: z2.number().int().min(1e5).max(2e7).optional(),
2934
2978
  format: z2.enum(["png", "jpeg"]).optional(),
@@ -2958,6 +3002,7 @@ var BrowserActionFieldsSchema = z2.object({
2958
3002
  cookiePath: BoundedString(2e3).optional(),
2959
3003
  cookieSecure: z2.boolean().optional(),
2960
3004
  cookieHttpOnly: z2.boolean().optional(),
3005
+ cookieSameSite: z2.enum(["Strict", "Lax", "None"]).optional(),
2961
3006
  storageArea: z2.enum(["local", "session"]).optional(),
2962
3007
  storageKey: BoundedString(1e3).optional(),
2963
3008
  storageValue: z2.string().max(2e4).optional(),
@@ -3050,13 +3095,54 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
3050
3095
  if (input.cookieValue !== void 0 && input.value !== void 0 && input.action === "set_cookie") {
3051
3096
  context.addIssue({ code: "custom", message: "Provide cookieValue or value, not both." });
3052
3097
  }
3098
+ if (input.cookieSameSite !== void 0 && input.action !== "set_cookie") {
3099
+ context.addIssue({ code: "custom", message: "cookieSameSite is only valid for set_cookie." });
3100
+ }
3053
3101
  if (input.storageValue !== void 0 && input.value !== void 0 && input.action === "set_storage") {
3054
3102
  context.addIssue({ code: "custom", message: "Provide storageValue or value, not both." });
3055
3103
  }
3056
3104
  if (input.outputPath !== void 0 && input.filePath !== void 0 && input.action === "save_as_pdf") {
3057
3105
  context.addIssue({ code: "custom", message: "Provide outputPath or filePath, not both." });
3058
3106
  }
3059
- if (["navigate", "set_cookie"].includes(input.action) && input.url !== void 0 && !isHttpUrl(input.url)) {
3107
+ if (input.action === "upload_file") {
3108
+ const hasFilePath = input.filePath !== void 0;
3109
+ const hasFilePaths = input.filePaths !== void 0;
3110
+ if (hasFilePath && hasFilePaths) {
3111
+ context.addIssue({ code: "custom", message: "Provide filePath or filePaths, not both." });
3112
+ }
3113
+ if (!hasFilePath && !hasFilePaths) {
3114
+ context.addIssue({ code: "custom", message: "Upload requires filePath or filePaths." });
3115
+ }
3116
+ } else if (input.filePaths !== void 0) {
3117
+ context.addIssue({ code: "custom", message: "filePaths is only valid for upload_file." });
3118
+ }
3119
+ if (input.action === "resource_blocking") {
3120
+ if (input.operation === void 0) {
3121
+ context.addIssue({ code: "custom", message: "Resource blocking requires operation." });
3122
+ } else if (input.operation === "set") {
3123
+ if (input.resourceTypes === void 0) {
3124
+ context.addIssue({ code: "custom", message: "Resource blocking set requires resourceTypes." });
3125
+ } else if (new Set(input.resourceTypes).size !== input.resourceTypes.length) {
3126
+ context.addIssue({ code: "custom", message: "Resource blocking resourceTypes must be de-duplicated." });
3127
+ }
3128
+ } else if (input.resourceTypes !== void 0) {
3129
+ context.addIssue({ code: "custom", message: `Resource blocking ${input.operation} does not accept resourceTypes.` });
3130
+ }
3131
+ } else {
3132
+ if (input.operation !== void 0) {
3133
+ context.addIssue({ code: "custom", message: "operation is only valid for resource blocking." });
3134
+ }
3135
+ if (input.resourceTypes !== void 0) {
3136
+ context.addIssue({ code: "custom", message: "resourceTypes is only valid for resource blocking." });
3137
+ }
3138
+ }
3139
+ if (input.action !== "inspect_element" && input.maxDepth !== void 0) {
3140
+ context.addIssue({ code: "custom", message: "maxDepth is only valid for inspect_element." });
3141
+ }
3142
+ if (input.action !== "inspect_element" && input.maxChildren !== void 0) {
3143
+ context.addIssue({ code: "custom", message: "maxChildren is only valid for inspect_element." });
3144
+ }
3145
+ if (["navigate", "get_cookies", "set_cookie", "delete_cookies"].includes(input.action) && input.url !== void 0 && !isHttpUrl(input.url)) {
3060
3146
  context.addIssue({ code: "custom", message: "Navigation and cookie URLs must be absolute HTTP(S) URLs." });
3061
3147
  }
3062
3148
  if (input.action === "click") {
@@ -3113,6 +3199,7 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
3113
3199
  case "wait_for_element":
3114
3200
  case "dropdown_options":
3115
3201
  case "find_elements":
3202
+ case "inspect_element":
3116
3203
  case "get_computed_style":
3117
3204
  case "hover":
3118
3205
  case "press_and_hold":
@@ -3127,7 +3214,7 @@ var BrowserActionSchema = BrowserActionFieldsSchema.extend({ action: ActionNameS
3127
3214
  break;
3128
3215
  case "upload_file":
3129
3216
  requireOne([input.target, input.ref, input.selector, input.index], "Upload requires target, ref, selector, or index.");
3130
- requireOne([input.filePath], "Upload requires filePath.");
3217
+ requireOne([input.filePath, input.filePaths], "Upload requires filePath or filePaths.");
3131
3218
  break;
3132
3219
  case "save_as_pdf":
3133
3220
  requireOne([input.outputPath, input.filePath], "PDF export requires outputPath.");
@@ -3215,6 +3302,7 @@ function normalizeBrowserActionInput(value) {
3215
3302
  moveActionField(output, "cookiePath", "path", issues);
3216
3303
  moveActionField(output, "cookieSecure", "secure", issues);
3217
3304
  moveActionField(output, "cookieHttpOnly", "httpOnly", issues);
3305
+ moveActionField(output, "cookieSameSite", "sameSite", issues);
3218
3306
  } else if (rawAction === "storage") {
3219
3307
  moveActionField(output, "storageArea", "area", issues);
3220
3308
  moveActionField(output, "storageKey", "key", issues);
@@ -3373,6 +3461,21 @@ var TargetRequestSchema = TargetFormSchema.superRefine((input, context) => {
3373
3461
  }
3374
3462
  });
3375
3463
  var SelectorRequestSchema = z2.object({ selector: BoundedString(2e3), ...PageInput }).strict();
3464
+ var InspectElementTargetFieldsSchema = z2.object({
3465
+ target: BoundedString(2e3).optional(),
3466
+ ref: z2.string().trim().min(1).max(200).regex(/^(?:ref:)?e[1-9]\d*$/, "ref must be an element reference such as e5.").optional(),
3467
+ selector: BoundedString(2e3).optional(),
3468
+ index: z2.number().int().min(0).max(1e3).optional(),
3469
+ maxDepth: z2.number().int().min(0).max(3).optional(),
3470
+ maxChildren: z2.number().int().min(1).max(100).optional(),
3471
+ ...PageInput
3472
+ }).strict();
3473
+ var InspectElementRequestSchema = InspectElementTargetFieldsSchema.superRefine((input, context) => {
3474
+ const targetCount = [input.target, input.ref, input.selector, input.index].filter((value) => value !== void 0).length;
3475
+ if (targetCount !== 1) {
3476
+ context.addIssue({ code: "custom", message: "Provide exactly one of target, ref, selector, or index." });
3477
+ }
3478
+ });
3376
3479
  var WaitRequestSchema = z2.object({ milliseconds: z2.number().int().min(0).max(12e4).default(500), ...PageInput }).strict();
3377
3480
  var WaitForTextRequestSchema = z2.object({ text: BoundedString(2e4), timeoutMs: z2.number().int().min(100).max(12e4).optional(), ...PageInput }).strict();
3378
3481
  var WaitForUrlRequestSchema = z2.object({ url: BoundedString(8e3), timeoutMs: z2.number().int().min(100).max(12e4).optional(), ...PageInput }).strict();
@@ -3420,7 +3523,16 @@ var ScreenshotRequestSchema = z2.object({ fullPage: z2.boolean().optional(), ful
3420
3523
  }
3421
3524
  });
3422
3525
  var PdfRequestSchema = z2.object({ outputPath: BoundedString(4e3), ...PageInput }).strict();
3423
- var UploadRequestSchema = z2.object({ selector: BoundedString(2e3), filePath: BoundedString(4e3), ...PageInput }).strict();
3526
+ var UploadRequestSchema = z2.object({ selector: BoundedString(2e3), filePath: BoundedString(4e3).optional(), filePaths: z2.array(BoundedString(4e3)).min(1).max(20).optional(), ...PageInput }).strict().superRefine((input, context) => {
3527
+ const hasFilePath = input.filePath !== void 0;
3528
+ const hasFilePaths = input.filePaths !== void 0;
3529
+ if (hasFilePath && hasFilePaths) {
3530
+ context.addIssue({ code: "custom", message: "Provide filePath or filePaths, not both." });
3531
+ }
3532
+ if (!hasFilePath && !hasFilePaths) {
3533
+ context.addIssue({ code: "custom", message: "Upload requires filePath or filePaths." });
3534
+ }
3535
+ });
3424
3536
  var EvaluateRequestSchema = z2.object({
3425
3537
  code: z2.string().trim().min(1).max(4e4).optional(),
3426
3538
  expression: z2.string().trim().min(1).max(4e4).optional(),
@@ -3434,6 +3546,32 @@ var EvaluateRequestSchema = z2.object({
3434
3546
  }
3435
3547
  });
3436
3548
  var NetworkLogRequestSchema = z2.object({ operation: z2.enum(["enable", "disable", "read", "clear", "read_and_clear"]), ...PageInput }).strict();
3549
+ var NetworkSearchRequestSchema = z2.object({
3550
+ query: BoundedString(4e3).optional(),
3551
+ requestId: BoundedString(256).optional(),
3552
+ url: BoundedString(8e3).optional(),
3553
+ method: BoundedString(32).optional(),
3554
+ status: z2.number().int().min(0).max(999).optional(),
3555
+ resourceType: BoundedString(64).optional(),
3556
+ offset: z2.number().int().min(0).max(1e6).optional(),
3557
+ limit: z2.number().int().min(1).max(200).optional(),
3558
+ pageId: BoundedString(200).optional()
3559
+ }).strict();
3560
+ var ResourceBlockingRequestSchema = z2.object({
3561
+ operation: ResourceBlockingOperationSchema,
3562
+ resourceTypes: z2.array(ResourceBlockingTypeSchema).min(1).max(RESOURCE_BLOCKING_TYPES.length).optional(),
3563
+ ...PageInput
3564
+ }).strict().superRefine((input, context) => {
3565
+ if (input.operation === "set") {
3566
+ if (input.resourceTypes === void 0) {
3567
+ context.addIssue({ code: "custom", message: "Resource blocking set requires resourceTypes." });
3568
+ } else if (new Set(input.resourceTypes).size !== input.resourceTypes.length) {
3569
+ context.addIssue({ code: "custom", message: "Resource blocking resourceTypes must be de-duplicated." });
3570
+ }
3571
+ } else if (input.resourceTypes !== void 0) {
3572
+ context.addIssue({ code: "custom", message: `Resource blocking ${input.operation} does not accept resourceTypes.` });
3573
+ }
3574
+ });
3437
3575
  var DialogRequestSchema = z2.object({ operation: z2.enum(["get_text", "accept", "dismiss", "send_keys"]), text: z2.string().max(2e4).optional(), ...PageInput }).strict().superRefine((input, context) => {
3438
3576
  if (input.operation === "send_keys" && input.text === void 0) {
3439
3577
  context.addIssue({ code: "custom", message: "Dialog send_keys requires text." });
@@ -3451,6 +3589,7 @@ var CookieRequestSchema = z2.object({
3451
3589
  url: HttpUrl(8e3).optional(),
3452
3590
  secure: z2.boolean().optional(),
3453
3591
  httpOnly: z2.boolean().optional(),
3592
+ sameSite: z2.enum(["Strict", "Lax", "None"]).optional(),
3454
3593
  ...PageInput
3455
3594
  }).strict().superRefine((input, context) => {
3456
3595
  if ((input.operation === "set" || input.operation === "delete") && !input.name) {
@@ -3459,6 +3598,9 @@ var CookieRequestSchema = z2.object({
3459
3598
  if (input.operation === "set" && input.value === void 0) {
3460
3599
  context.addIssue({ code: "custom", message: "Cookie set requires value." });
3461
3600
  }
3601
+ if (input.sameSite !== void 0 && input.operation !== "set") {
3602
+ context.addIssue({ code: "custom", message: "Cookie sameSite is only valid for set." });
3603
+ }
3462
3604
  });
3463
3605
  var StorageRequestSchema = z2.object({
3464
3606
  operation: z2.enum(["get", "set", "clear"]),
@@ -3838,7 +3980,7 @@ function registerBrowserTools(server, runtime) {
3838
3980
  "browser_get_html",
3839
3981
  {
3840
3982
  title: "Read page HTML",
3841
- description: "Read bounded raw HTML (at most 8,000 characters) for the current page or a CSS selector. Check the explicit truncated flag before relying on completeness; HTML is untrusted data and is never executed by this tool.",
3983
+ description: "Read a bounded sanitized HTML projection (at most 8,000 characters) for the current page or a CSS selector. Scripts, event handlers, form values/textarea contents, and other unsafe attributes are omitted. Check the explicit truncated flag before relying on completeness; HTML is untrusted data and is never executed by this tool.",
3842
3984
  inputSchema: HtmlRequestSchema,
3843
3985
  annotations: BROWSER_READ_ONLY
3844
3986
  },
@@ -3888,6 +4030,16 @@ function registerBrowserTools(server, runtime) {
3888
4030
  { title: "Read browser network log", description: "Enable, disable, read, clear, or read-and-clear the redacted network log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
3889
4031
  async (input, ctx) => callTool(() => runtime.run({ action: networkAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
3890
4032
  );
4033
+ server.registerTool(
4034
+ "browser_search_network_log",
4035
+ { title: "Search browser network log", description: "Search the bounded redacted network journal by text, request ID, URL, method, status, or resource type. Results are deterministic and expose explicit capacity and omission metadata.", inputSchema: NetworkSearchRequestSchema, annotations: BROWSER_READ_ONLY },
4036
+ async (input, ctx) => callTool(() => runtime.run({ action: "search_network_log", ...input }, ctx.mcpReq.signal), runtime)
4037
+ );
4038
+ server.registerTool(
4039
+ "browser_resource_blocking",
4040
+ { title: "Configure resource blocking", description: "Get, set, or clear page-scoped blocking for image, stylesheet, font, media, and script subresources. Navigation and document requests are never blocked by this tool.", inputSchema: ResourceBlockingRequestSchema, annotations: BROWSER_MUTATING },
4041
+ async (input, ctx) => callTool(() => runtime.run({ action: "resource_blocking", operation: input.operation, resourceTypes: input.resourceTypes, pageId: input.pageId }, ctx.mcpReq.signal), runtime)
4042
+ );
3891
4043
  server.registerTool(
3892
4044
  "browser_console_log",
3893
4045
  { title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
@@ -3898,7 +4050,7 @@ function registerBrowserTools(server, runtime) {
3898
4050
  return { ...fields, text: query };
3899
4051
  });
3900
4052
  registerAction(server, runtime, "browser_extract", "Extract page text", "Extract at most 8,000 page-text characters from the page or a CSS selector. Check truncated, offset, nextOffset, hasMore, and revision; use browser_page_next for later slices.", ExtractRequestSchema, "extract", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
3901
- registerAction(server, runtime, "browser_upload", "Upload a file", "Upload a file from an allowed server file root into a file input.", UploadRequestSchema, "upload_file");
4053
+ registerAction(server, runtime, "browser_upload", "Upload files", "Upload one file or up to 20 files from allowed server file roots into a file input; multiple files require the input's multiple attribute.", UploadRequestSchema, "upload_file");
3902
4054
  registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
3903
4055
  const { full_page, full, max_bytes, max_dim, ...fields } = input;
3904
4056
  return { ...fields, fullPage: fields.fullPage ?? full_page ?? full, maxBytes: fields.maxBytes ?? max_bytes, maxDimension: fields.maxDimension ?? max_dim };
@@ -3909,6 +4061,7 @@ function registerBrowserTools(server, runtime) {
3909
4061
  registerAction(server, runtime, "browser_page_next", "Read the next page slice", "Read at most 8,000 characters from the current page at offset and revision. Advance to nextOffset only when hasMore is true; stale revisions are retryable and page text is untrusted.", PageNextSchema, "page_next", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
3910
4062
  registerAction(server, runtime, "browser_search_page", "Search the current page", "Find bounded snippets for a query in current-page text.", PageQuerySchema, "search_page");
3911
4063
  registerAction(server, runtime, "browser_find_elements", "Find elements", "List bounded element metadata for a CSS selector.", SelectorRequestSchema, "find_elements");
4064
+ registerAction(server, runtime, "browser_inspect_element", "Inspect an element", "Read bounded safe attributes, selected computed styles, pseudo-element summaries, animation metadata, and shallow child structure for a current selector, ref, or index. Scripts, event-handler source, form values, and arbitrary data attributes are omitted.", InspectElementRequestSchema, "inspect_element");
3912
4065
  registerAction(server, runtime, "browser_interactive", "List interactive elements", "List visible links, buttons, inputs, and other interactive elements with stable refs.", EmptyInputSchema, "list_interactive");
3913
4066
  registerAction(server, runtime, "browser_frames", "List browser frames", "List bounded frame metadata for the current page. Frame content is not returned by this metadata tool.", EmptyInputSchema, "list_frames");
3914
4067
  registerAction(server, runtime, "browser_accessibility_snapshot", "Read accessibility tree", "Read a bounded accessibility tree through Chrome DevTools. Check truncation before relying on completeness; AX refs are observation-only and must be revalidated through DOM refs before acting.", AccessibilityRequestSchema, "accessibility_snapshot", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
@@ -4023,12 +4176,14 @@ function actionAnnotations(action) {
4023
4176
  case "page_next":
4024
4177
  case "search_page":
4025
4178
  case "find_elements":
4179
+ case "inspect_element":
4026
4180
  case "list_interactive":
4027
4181
  case "list_frames":
4028
4182
  case "accessibility_snapshot":
4029
4183
  case "get_computed_style":
4030
4184
  case "get_page_info":
4031
4185
  case "get_network_log":
4186
+ case "search_network_log":
4032
4187
  case "get_console_log":
4033
4188
  case "alert_get_text":
4034
4189
  case "detect_challenge":
@@ -4077,7 +4232,8 @@ function cookieAction(input) {
4077
4232
  cookiePath: input.path,
4078
4233
  url: input.url,
4079
4234
  cookieSecure: input.secure,
4080
- cookieHttpOnly: input.httpOnly
4235
+ cookieHttpOnly: input.httpOnly,
4236
+ cookieSameSite: input.sameSite
4081
4237
  };
4082
4238
  }
4083
4239
  function storageAction(input) {
@@ -4384,6 +4540,13 @@ function boundMcpOutput(value, options = {}) {
4384
4540
  if (typeof output.warning !== "string") {
4385
4541
  output.warning = "Some search results were omitted by the MCP output limit; use a narrower request or a paginated tool.";
4386
4542
  }
4543
+ } else if (key === "entries") {
4544
+ output.hasMore = true;
4545
+ if (typeof output.returnedCount === "number" && Number.isFinite(output.returnedCount)) {
4546
+ output.returnedCount = Math.min(Math.max(0, Math.trunc(output.returnedCount)), limit);
4547
+ }
4548
+ const previousOmittedCount = typeof output.omittedCount === "number" && Number.isSafeInteger(output.omittedCount) ? Math.max(0, output.omittedCount) : 0;
4549
+ output.omittedCount = previousOmittedCount + omitted;
4387
4550
  }
4388
4551
  markOutputTruncated();
4389
4552
  }
@@ -4683,11 +4846,31 @@ var DEFAULT_TYPE = {
4683
4846
  rng: Math.random
4684
4847
  };
4685
4848
  function randomRange(min, max, rand = Math.random) {
4686
- return min + rand() * (max - min);
4849
+ const sample = rand();
4850
+ const boundedSample = Number.isFinite(sample) ? Math.min(1, Math.max(0, sample)) : 0;
4851
+ return min + boundedSample * (max - min);
4687
4852
  }
4688
- function sleep(ms) {
4689
- return new Promise((resolve7) => {
4690
- setTimeout(resolve7, Math.max(0, ms));
4853
+ function sleep(ms, signal) {
4854
+ if (signal?.aborted) {
4855
+ return Promise.reject(new Error("Operation aborted"));
4856
+ }
4857
+ return new Promise((resolve7, reject) => {
4858
+ let settled = false;
4859
+ const timer = setTimeout(() => finish(resolve7), Math.max(0, ms));
4860
+ const onAbort = () => finish(() => reject(new Error("Operation aborted")));
4861
+ const finish = (callback) => {
4862
+ if (settled) {
4863
+ return;
4864
+ }
4865
+ settled = true;
4866
+ clearTimeout(timer);
4867
+ signal?.removeEventListener("abort", onAbort);
4868
+ callback();
4869
+ };
4870
+ signal?.addEventListener("abort", onAbort, { once: true });
4871
+ if (signal?.aborted) {
4872
+ onAbort();
4873
+ }
4691
4874
  });
4692
4875
  }
4693
4876
  async function humanMouseMove(page, x1, y1, x2, y2, durationMs = 80, options = {}) {
@@ -4706,15 +4889,18 @@ async function humanType(page, text, options = {}) {
4706
4889
  const cfg = { ...DEFAULT_TYPE, ...options, rng };
4707
4890
  const keyboard = page.keyboard;
4708
4891
  for (const char of text) {
4892
+ if (cfg.signal?.aborted) {
4893
+ throw new Error("Operation aborted");
4894
+ }
4709
4895
  if (char === " ") {
4710
4896
  await keyboard.down("Space");
4711
4897
  await keyboard.up("Space");
4712
4898
  } else {
4713
4899
  await keyboard.type(char);
4714
4900
  }
4715
- await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng));
4901
+ await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng), cfg.signal);
4716
4902
  if (cfg.rng() < cfg.thinkPauseChance) {
4717
- await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng));
4903
+ await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng), cfg.signal);
4718
4904
  }
4719
4905
  }
4720
4906
  }
@@ -5055,6 +5241,294 @@ function globMatches(value, glob) {
5055
5241
  return compiled.test(value);
5056
5242
  }
5057
5243
 
5244
+ // src/server/browser/network.ts
5245
+ var DEFAULT_CAPACITY = 500;
5246
+ var MAX_CAPACITY = 1e4;
5247
+ var DEFAULT_MAX_PAGES = 128;
5248
+ var MAX_MAX_PAGES = 1024;
5249
+ var DEFAULT_LIMIT = 50;
5250
+ var MAX_LIMIT = 200;
5251
+ var MAX_PAGE_ID_CHARS = 200;
5252
+ var MAX_REQUEST_ID_CHARS = 256;
5253
+ var MAX_METHOD_CHARS = 32;
5254
+ var MAX_RESOURCE_TYPE_CHARS = 64;
5255
+ var NetworkJournal = class {
5256
+ capacity;
5257
+ maxPages;
5258
+ pages = /* @__PURE__ */ new Map();
5259
+ generatedRequestSequence = 0;
5260
+ evictedPageCount = 0;
5261
+ constructor(options = {}) {
5262
+ this.capacity = boundedPositiveInteger(options.capacity ?? DEFAULT_CAPACITY, MAX_CAPACITY, "capacity");
5263
+ this.maxPages = boundedPositiveInteger(options.maxPages ?? DEFAULT_MAX_PAGES, MAX_MAX_PAGES, "maxPages");
5264
+ }
5265
+ /** Record or update request metadata and return an immutable snapshot. */
5266
+ recordRequest(event) {
5267
+ const pageId = normalizeRequiredIdentifier(event?.pageId, "pageId", MAX_PAGE_ID_CHARS);
5268
+ const page = this.ensurePage(pageId);
5269
+ const requestId = this.resolveRequestId(pageId, event?.requestId);
5270
+ const existing = page.entries.get(requestId);
5271
+ const timestamp = normalizeTimestamp(event?.timestamp);
5272
+ const resourceType = normalizeOptionalText(event?.resourceType, MAX_RESOURCE_TYPE_CHARS) ?? existing?.entry.resourceType;
5273
+ const entry = {
5274
+ pageId,
5275
+ requestId,
5276
+ url: safeNetworkUrl(event?.url),
5277
+ method: normalizeMethod(event?.method),
5278
+ ...resourceType ? { resourceType } : {},
5279
+ ...existing?.entry.status !== void 0 ? { status: existing.entry.status } : {},
5280
+ requestTimestamp: existing?.entry.requestTimestamp ?? timestamp,
5281
+ ...existing?.entry.responseTimestamp ? { responseTimestamp: existing.entry.responseTimestamp } : {}
5282
+ };
5283
+ page.entries.set(requestId, this.stored(entry));
5284
+ this.enforcePageCapacity(page);
5285
+ return cloneEntry(page.entries.get(requestId)?.entry ?? entry);
5286
+ }
5287
+ /** Record or update response metadata and correlate it to its request. */
5288
+ recordResponse(event) {
5289
+ const pageId = normalizeRequiredIdentifier(event?.pageId, "pageId", MAX_PAGE_ID_CHARS);
5290
+ const page = this.ensurePage(pageId);
5291
+ const requestId = normalizeRequiredIdentifier(event?.requestId, "requestId", MAX_REQUEST_ID_CHARS);
5292
+ const existing = page.entries.get(requestId);
5293
+ const timestamp = normalizeTimestamp(event?.timestamp);
5294
+ const entry = existing ? {
5295
+ ...existing.entry,
5296
+ ...event.url !== void 0 ? { url: safeNetworkUrl(event.url) } : {},
5297
+ ...event.resourceType !== void 0 ? { resourceType: normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) } : {},
5298
+ ...isValidStatus(event.status) ? { status: event.status } : {},
5299
+ responseTimestamp: timestamp
5300
+ } : {
5301
+ pageId,
5302
+ requestId,
5303
+ url: event.url === void 0 ? "[URL_UNAVAILABLE]" : safeNetworkUrl(event.url),
5304
+ method: "UNKNOWN",
5305
+ ...normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) ? { resourceType: normalizeOptionalText(event.resourceType, MAX_RESOURCE_TYPE_CHARS) } : {},
5306
+ ...isValidStatus(event.status) ? { status: event.status } : {},
5307
+ requestTimestamp: timestamp,
5308
+ responseTimestamp: timestamp
5309
+ };
5310
+ page.entries.set(requestId, this.stored(entry));
5311
+ this.enforcePageCapacity(page);
5312
+ return cloneEntry(page.entries.get(requestId)?.entry ?? entry);
5313
+ }
5314
+ /** Query retained records using deterministic metadata filters and paging. */
5315
+ query(query = {}) {
5316
+ const normalized = normalizeQuery(query);
5317
+ const selectedPages = normalized.pageId === void 0 ? [...this.pages.entries()] : [[normalized.pageId, this.pages.get(normalized.pageId)]];
5318
+ const retainedCount = selectedPages.reduce((total, [, page]) => total + (page?.entries.size ?? 0), 0);
5319
+ const evictedCount = selectedPages.reduce((total, [, page]) => total + (page?.evictedCount ?? 0), 0) + (normalized.pageId === void 0 ? this.evictedPageCount : 0);
5320
+ const capacityReached = selectedPages.some(([, page]) => (page?.entries.size ?? 0) >= this.capacity || (page?.evictedCount ?? 0) > 0);
5321
+ const matches = [];
5322
+ for (const [, page] of selectedPages) {
5323
+ if (!page) continue;
5324
+ for (const stored of page.entries.values()) {
5325
+ if (matchesFilter(stored.entry, normalized)) {
5326
+ matches.push(stored.entry);
5327
+ }
5328
+ }
5329
+ }
5330
+ const entries = matches.slice(normalized.offset, normalized.offset + normalized.limit).map(cloneEntry);
5331
+ return {
5332
+ entries,
5333
+ offset: normalized.offset,
5334
+ limit: normalized.limit,
5335
+ total: matches.length,
5336
+ returnedCount: entries.length,
5337
+ omittedCount: Math.max(0, matches.length - entries.length),
5338
+ hasMore: normalized.offset + entries.length < matches.length,
5339
+ retainedCount,
5340
+ capacity: this.capacity,
5341
+ evictedCount,
5342
+ capacityReached
5343
+ };
5344
+ }
5345
+ /** Search all safe metadata fields using one bounded case-insensitive scan. */
5346
+ search(searchText, options = {}) {
5347
+ const query = normalizeSearchText(searchText);
5348
+ if (!query) {
5349
+ throw new RangeError("searchText must be a non-empty string.");
5350
+ }
5351
+ const normalized = normalizeQuery(options);
5352
+ const selectedPages = normalized.pageId === void 0 ? [...this.pages.entries()] : [[normalized.pageId, this.pages.get(normalized.pageId)]];
5353
+ const matches = [];
5354
+ for (const [, page] of selectedPages) {
5355
+ if (!page) continue;
5356
+ for (const stored of page.entries.values()) {
5357
+ if (stored.searchText.includes(query) && matchesFilter(stored.entry, normalized)) {
5358
+ matches.push(stored.entry);
5359
+ }
5360
+ }
5361
+ }
5362
+ return this.pageFromMatches(matches, normalized, selectedPages);
5363
+ }
5364
+ /** Remove all records, or only records associated with one page. */
5365
+ clear(pageId) {
5366
+ if (pageId === void 0) {
5367
+ const clearedCount2 = [...this.pages.values()].reduce((total, page2) => total + page2.entries.size, 0);
5368
+ this.pages.clear();
5369
+ this.evictedPageCount = 0;
5370
+ return { clearedCount: clearedCount2, retainedCount: 0 };
5371
+ }
5372
+ const normalizedPageId = normalizeRequiredIdentifier(pageId, "pageId", MAX_PAGE_ID_CHARS);
5373
+ const page = this.pages.get(normalizedPageId);
5374
+ const clearedCount = page?.entries.size ?? 0;
5375
+ this.pages.delete(normalizedPageId);
5376
+ return { clearedCount, retainedCount: this.retainedCount() };
5377
+ }
5378
+ /** Return bounded journal counts without exposing records. */
5379
+ stats(pageId) {
5380
+ const result = this.query(pageId === void 0 ? {} : { pageId, limit: 1 });
5381
+ return {
5382
+ retainedCount: result.retainedCount,
5383
+ capacity: result.capacity,
5384
+ evictedCount: result.evictedCount,
5385
+ capacityReached: result.capacityReached
5386
+ };
5387
+ }
5388
+ pageFromMatches(matches, query, selectedPages) {
5389
+ const entries = matches.slice(query.offset, query.offset + query.limit).map(cloneEntry);
5390
+ const retainedCount = selectedPages.reduce((total, [, page]) => total + (page?.entries.size ?? 0), 0);
5391
+ const evictedCount = selectedPages.reduce((total, [, page]) => total + (page?.evictedCount ?? 0), 0) + (query.pageId === void 0 ? this.evictedPageCount : 0);
5392
+ return {
5393
+ entries,
5394
+ offset: query.offset,
5395
+ limit: query.limit,
5396
+ total: matches.length,
5397
+ returnedCount: entries.length,
5398
+ omittedCount: Math.max(0, matches.length - entries.length),
5399
+ hasMore: query.offset + entries.length < matches.length,
5400
+ retainedCount,
5401
+ capacity: this.capacity,
5402
+ evictedCount,
5403
+ capacityReached: selectedPages.some(([, page]) => (page?.entries.size ?? 0) >= this.capacity || (page?.evictedCount ?? 0) > 0)
5404
+ };
5405
+ }
5406
+ ensurePage(pageId) {
5407
+ const existing = this.pages.get(pageId);
5408
+ if (existing) return existing;
5409
+ while (this.pages.size >= this.maxPages) {
5410
+ const oldestPageId = this.pages.keys().next().value;
5411
+ if (oldestPageId === void 0) break;
5412
+ const oldest = this.pages.get(oldestPageId);
5413
+ this.evictedPageCount += oldest?.entries.size ?? 0;
5414
+ this.pages.delete(oldestPageId);
5415
+ }
5416
+ const page = { entries: /* @__PURE__ */ new Map(), evictedCount: 0 };
5417
+ this.pages.set(pageId, page);
5418
+ return page;
5419
+ }
5420
+ resolveRequestId(pageId, rawRequestId) {
5421
+ const normalized = normalizeOptionalIdentifier(rawRequestId, MAX_REQUEST_ID_CHARS);
5422
+ if (normalized) return normalized;
5423
+ this.generatedRequestSequence += 1;
5424
+ return `${pageId}:request-${this.generatedRequestSequence}`.slice(0, MAX_REQUEST_ID_CHARS);
5425
+ }
5426
+ stored(entry) {
5427
+ const searchParts = [entry.pageId, entry.requestId, entry.url, entry.method, entry.resourceType ?? "", entry.status === void 0 ? "" : String(entry.status)];
5428
+ return { entry, searchText: searchParts.join(" ").toLocaleLowerCase("en-US") };
5429
+ }
5430
+ enforcePageCapacity(page) {
5431
+ while (page.entries.size > this.capacity) {
5432
+ const oldestRequestId = page.entries.keys().next().value;
5433
+ if (oldestRequestId === void 0) break;
5434
+ page.entries.delete(oldestRequestId);
5435
+ page.evictedCount += 1;
5436
+ }
5437
+ }
5438
+ retainedCount() {
5439
+ return [...this.pages.values()].reduce((total, page) => total + page.entries.size, 0);
5440
+ }
5441
+ };
5442
+ function normalizeQuery(query) {
5443
+ if (query === null || typeof query !== "object") {
5444
+ throw new TypeError("query must be an object.");
5445
+ }
5446
+ const offset = boundedNonnegativeInteger(query.offset ?? 0, "offset");
5447
+ const limit = boundedPositiveInteger(query.limit ?? DEFAULT_LIMIT, MAX_LIMIT, "limit");
5448
+ return {
5449
+ ...query.pageId === void 0 ? {} : { pageId: normalizeRequiredIdentifier(query.pageId, "pageId", MAX_PAGE_ID_CHARS) },
5450
+ ...query.requestId === void 0 ? {} : { requestId: normalizeOptionalText(query.requestId, MAX_REQUEST_ID_CHARS) },
5451
+ ...query.url === void 0 ? {} : { url: normalizeSearchText(query.url) },
5452
+ ...query.method === void 0 ? {} : { method: normalizeMethod(query.method) },
5453
+ ...query.status === void 0 ? {} : { status: normalizeStatus(query.status) },
5454
+ ...query.resourceType === void 0 ? {} : { resourceType: normalizeOptionalText(query.resourceType, MAX_RESOURCE_TYPE_CHARS) },
5455
+ offset,
5456
+ limit
5457
+ };
5458
+ }
5459
+ function matchesFilter(entry, filter) {
5460
+ if (filter.pageId !== void 0 && entry.pageId !== filter.pageId) return false;
5461
+ if (filter.requestId !== void 0 && !entry.requestId.toLocaleLowerCase("en-US").includes(filter.requestId.toLocaleLowerCase("en-US"))) return false;
5462
+ if (filter.url !== void 0 && !entry.url.toLocaleLowerCase("en-US").includes(filter.url.toLocaleLowerCase("en-US"))) return false;
5463
+ if (filter.method !== void 0 && entry.method !== filter.method) return false;
5464
+ if (filter.status !== void 0 && entry.status !== filter.status) return false;
5465
+ if (filter.resourceType !== void 0 && entry.resourceType?.toLocaleLowerCase("en-US") !== filter.resourceType.toLocaleLowerCase("en-US")) return false;
5466
+ return true;
5467
+ }
5468
+ function cloneEntry(entry) {
5469
+ return { ...entry };
5470
+ }
5471
+ function safeNetworkUrl(rawUrl) {
5472
+ if (typeof rawUrl !== "string" || !rawUrl.trim()) return "[URL_UNAVAILABLE]";
5473
+ const trimmed = rawUrl.trim();
5474
+ let parsed;
5475
+ try {
5476
+ parsed = new URL(trimmed);
5477
+ } catch {
5478
+ return "[INVALID_URL]";
5479
+ }
5480
+ if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) {
5481
+ return "[NON_HTTP_URL]";
5482
+ }
5483
+ return redactSecretPlaceholders(sanitizeUrl(trimmed));
5484
+ }
5485
+ function normalizeRequiredIdentifier(value, name, maxChars) {
5486
+ const normalized = normalizeOptionalIdentifier(value, maxChars);
5487
+ if (!normalized) throw new TypeError(`${name} must be a non-empty string or number.`);
5488
+ return normalized;
5489
+ }
5490
+ function normalizeOptionalIdentifier(value, maxChars) {
5491
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
5492
+ if (typeof value === "number" && !Number.isSafeInteger(value)) return void 0;
5493
+ return normalizeOptionalText(String(value), maxChars);
5494
+ }
5495
+ function normalizeOptionalText(value, maxChars) {
5496
+ if (typeof value !== "string") return void 0;
5497
+ const normalized = value.normalize("NFKC").replace(/[\u0000-\u001f\u007f\u200b-\u200d\u2060\ufeff]/g, "").trim();
5498
+ return normalized ? normalized.slice(0, maxChars) : void 0;
5499
+ }
5500
+ function normalizeMethod(value) {
5501
+ return (normalizeOptionalText(value, MAX_METHOD_CHARS) ?? "UNKNOWN").toUpperCase();
5502
+ }
5503
+ function normalizeSearchText(value) {
5504
+ const normalized = normalizeOptionalText(value, 512);
5505
+ if (!normalized) throw new TypeError("search and filter values must be non-empty strings.");
5506
+ return normalized.toLocaleLowerCase("en-US");
5507
+ }
5508
+ function normalizeTimestamp(value) {
5509
+ const date = value instanceof Date ? value : typeof value === "number" && Number.isFinite(value) ? new Date(value) : typeof value === "string" && value.trim() ? new Date(value) : /* @__PURE__ */ new Date();
5510
+ return Number.isNaN(date.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : date.toISOString();
5511
+ }
5512
+ function isValidStatus(value) {
5513
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 999;
5514
+ }
5515
+ function normalizeStatus(value) {
5516
+ if (!isValidStatus(value)) throw new TypeError("status must be an integer between 0 and 999.");
5517
+ return value;
5518
+ }
5519
+ function boundedPositiveInteger(value, maximum, name) {
5520
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
5521
+ throw new RangeError(`${name} must be an integer between 1 and ${maximum}.`);
5522
+ }
5523
+ return value;
5524
+ }
5525
+ function boundedNonnegativeInteger(value, name) {
5526
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
5527
+ throw new RangeError(`${name} must be a non-negative integer.`);
5528
+ }
5529
+ return value;
5530
+ }
5531
+
5058
5532
  // src/server/browser/service.ts
5059
5533
  var puppeteerModulePromise;
5060
5534
  function loadPuppeteer() {
@@ -5069,6 +5543,52 @@ var POPUP_POST_CLICK_SETTLE_TIMEOUT_MS = 300;
5069
5543
  var MAX_DOM_TRAVERSAL_NODES = 2e4;
5070
5544
  var MAX_TEXT_SCAN_CHARS = 5e5;
5071
5545
  var MAX_MARKUP_EVIDENCE_CHARS = 12e4;
5546
+ var MAX_INSPECT_NODES = 512;
5547
+ var MAX_INSPECT_STRING_CHARS = 500;
5548
+ var SAFE_ELEMENT_ATTRIBUTE_NAMES = [
5549
+ "id",
5550
+ "class",
5551
+ "role",
5552
+ "type",
5553
+ "name",
5554
+ "placeholder",
5555
+ "title",
5556
+ "tabindex",
5557
+ "style",
5558
+ "fill",
5559
+ "stroke",
5560
+ "x",
5561
+ "y",
5562
+ "x1",
5563
+ "x2",
5564
+ "y1",
5565
+ "y2",
5566
+ "r",
5567
+ "cx",
5568
+ "cy",
5569
+ "width",
5570
+ "height",
5571
+ "points",
5572
+ "transform",
5573
+ "font-size"
5574
+ ];
5575
+ var SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES = [
5576
+ "data-color",
5577
+ "data-index",
5578
+ "data-sides",
5579
+ "data-result",
5580
+ "data-key",
5581
+ "data-type",
5582
+ "data-item",
5583
+ "data-id",
5584
+ "data-start",
5585
+ "data-end",
5586
+ "data-duration",
5587
+ "data-output",
5588
+ "data-value",
5589
+ "data-position",
5590
+ "data-price"
5591
+ ];
5072
5592
  var CHALLENGE_AI_GUIDANCE = "Use normal browser click, input, scroll, or key tools on the visible challenge controls, then call solve_challenge again to verify that the challenge is cleared.";
5073
5593
  var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
5074
5594
  var CHALLENGE_MAX_ATTEMPTS = 100;
@@ -5081,6 +5601,11 @@ var NAVIGATION_CLICK_SETTLE_TIMEOUT_MS = 50;
5081
5601
  var NAVIGATION_CLICK_EVENT_TIMEOUT_MS = 250;
5082
5602
  var NAVIGATION_CLICK_READY_TIMEOUT_MS = 250;
5083
5603
  var SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS = 1e3;
5604
+ var MIN_IDLE_SWEEP_INTERVAL_MS = 250;
5605
+ var MAX_IDLE_SWEEP_INTERVAL_MS = 6e4;
5606
+ var MAX_DEVTOOLS_PROBE_RESPONSE_BYTES = 64 * 1024;
5607
+ var MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
5608
+ var MAX_UPLOAD_TOTAL_BYTES = 100 * 1024 * 1024;
5084
5609
  var COMMON_KEY_ALIASES = {
5085
5610
  ALT: "Alt",
5086
5611
  ARROWDOWN: "ArrowDown",
@@ -5167,13 +5692,15 @@ var PARALLEL_READ_ACTIONS = /* @__PURE__ */ new Set([
5167
5692
  "page_next",
5168
5693
  "search_page",
5169
5694
  "find_elements",
5695
+ "inspect_element",
5170
5696
  "list_frames",
5171
5697
  "accessibility_snapshot",
5172
5698
  "get_computed_style",
5173
5699
  "get_page_info",
5174
5700
  "get_cookies",
5175
5701
  "get_storage",
5176
- "list_downloads"
5702
+ "list_downloads",
5703
+ "search_network_log"
5177
5704
  ]);
5178
5705
  var BrowserService = class {
5179
5706
  constructor(config, policy, logger, dependencies = {}) {
@@ -5181,6 +5708,7 @@ var BrowserService = class {
5181
5708
  this.policy = policy;
5182
5709
  this.logger = logger;
5183
5710
  this.dependencies = dependencies;
5711
+ this.startIdleSweep();
5184
5712
  }
5185
5713
  config;
5186
5714
  policy;
@@ -5211,6 +5739,7 @@ var BrowserService = class {
5211
5739
  currentPageId;
5212
5740
  sessionGeneration = 0;
5213
5741
  states = /* @__PURE__ */ new Map();
5742
+ networkJournal = new NetworkJournal();
5214
5743
  configuredDownloadContexts = /* @__PURE__ */ new WeakSet();
5215
5744
  // The download directory is process/session scoped, while page setup is
5216
5745
  // page scoped. Share the mkdir promise across pages so opening a tab does
@@ -5218,9 +5747,11 @@ var BrowserService = class {
5218
5747
  // so a later page can retry after a transient filesystem failure.
5219
5748
  downloadDirectoryPromise;
5220
5749
  ids = /* @__PURE__ */ new WeakMap();
5750
+ networkRequestIds = /* @__PURE__ */ new WeakMap();
5221
5751
  targetGuardSessions = /* @__PURE__ */ new Map();
5222
5752
  targetGuardNavigationErrors = /* @__PURE__ */ new Map();
5223
5753
  unguardedTargetSessions = /* @__PURE__ */ new Set();
5754
+ handledTargetGuardSessions = /* @__PURE__ */ new Set();
5224
5755
  pendingTargetGuardSessions = /* @__PURE__ */ new Map();
5225
5756
  pendingTargetGuardInfos = /* @__PURE__ */ new Map();
5226
5757
  targetGuardUnavailable = false;
@@ -5228,11 +5759,134 @@ var BrowserService = class {
5228
5759
  targetGuardConnectionListener;
5229
5760
  targetGuardRawConnectionListener;
5230
5761
  targetGuardDetachedListener;
5762
+ targetGuardReadinessPromise;
5231
5763
  targetGuardOriginalEmit;
5232
5764
  targetGuardWrappedEmit;
5233
5765
  operationTail = Promise.resolve();
5234
5766
  queuedOperations = 0;
5235
5767
  benchmarkCounters = process.env.SMOOTH_OPERATOR_BENCHMARK_COUNTERS === "true" ? { browserOperations: 0, pageLookups: 0, pageEnumerations: 0, pageEvaluations: 0, cdpCommands: 0 } : void 0;
5768
+ idleSweepTimer;
5769
+ idleSweepPromise;
5770
+ idleSweepStopped = false;
5771
+ pendingTargetPreparations = /* @__PURE__ */ new Set();
5772
+ startIdleSweep() {
5773
+ const idleTimeoutMs = this.config.browser.idleTimeoutMs;
5774
+ if (this.config.browser.mode === "disabled" || idleTimeoutMs <= 0) {
5775
+ return;
5776
+ }
5777
+ const intervalMs = Math.min(
5778
+ MAX_IDLE_SWEEP_INTERVAL_MS,
5779
+ Math.max(MIN_IDLE_SWEEP_INTERVAL_MS, Math.floor(idleTimeoutMs / 2))
5780
+ );
5781
+ const timer = setInterval(() => this.scheduleIdleSweep(), intervalMs);
5782
+ timer.unref?.();
5783
+ this.idleSweepTimer = timer;
5784
+ }
5785
+ stopIdleSweep() {
5786
+ this.idleSweepStopped = true;
5787
+ if (this.idleSweepTimer) {
5788
+ clearInterval(this.idleSweepTimer);
5789
+ this.idleSweepTimer = void 0;
5790
+ }
5791
+ }
5792
+ scheduleIdleSweep() {
5793
+ if (this.idleSweepStopped || this.idleSweepPromise || !this.browser || this.browser.connected === false) {
5794
+ return;
5795
+ }
5796
+ if (this.queuedOperations > 0 || this.activeOperationControllers.size > 0) {
5797
+ return;
5798
+ }
5799
+ if (this.connectionPromise || this.connectionSettlementPromise || this.browserClosePromise || this.interruptedBrowserShutdown || this.recoveryPromise || this.failedBrowserShutdown || this.browserShutdownFailure || this.pendingTargetPreparations.size > 0) {
5800
+ return;
5801
+ }
5802
+ const observedActivityAt = this.lastActivityAt;
5803
+ const intervalMs = this.idleSweepIntervalMs();
5804
+ const sweep = this.withOperationLock(
5805
+ void 0,
5806
+ (signal) => this.sweepIdleBrowser(observedActivityAt, signal),
5807
+ intervalMs,
5808
+ intervalMs,
5809
+ "exclusive",
5810
+ false
5811
+ );
5812
+ this.idleSweepPromise = sweep;
5813
+ void sweep.then(
5814
+ () => {
5815
+ if (this.idleSweepPromise === sweep) {
5816
+ this.idleSweepPromise = void 0;
5817
+ }
5818
+ },
5819
+ (error) => {
5820
+ if (this.idleSweepPromise === sweep) {
5821
+ this.idleSweepPromise = void 0;
5822
+ }
5823
+ this.logger.debug("Idle browser sweep did not complete", { error: safeErrorDiagnostic(error) });
5824
+ }
5825
+ );
5826
+ }
5827
+ idleSweepIntervalMs() {
5828
+ const idleTimeoutMs = this.config.browser.idleTimeoutMs;
5829
+ return Math.min(
5830
+ MAX_IDLE_SWEEP_INTERVAL_MS,
5831
+ Math.max(MIN_IDLE_SWEEP_INTERVAL_MS, Math.floor(idleTimeoutMs / 2))
5832
+ );
5833
+ }
5834
+ async sweepIdleBrowser(observedActivityAt, signal) {
5835
+ throwIfAborted(signal);
5836
+ if (this.idleSweepStopped || this.shuttingDown || this.config.browser.idleTimeoutMs <= 0) {
5837
+ return;
5838
+ }
5839
+ const browser = this.browser;
5840
+ if (!browser || browser.connected === false) {
5841
+ return;
5842
+ }
5843
+ if (this.queuedOperations !== 1 || this.activeOperationControllers.size !== 1) {
5844
+ return;
5845
+ }
5846
+ if (this.connectionPromise || this.connectionSettlementPromise || this.browserClosePromise || this.interruptedBrowserShutdown || this.recoveryPromise || this.failedBrowserShutdown || this.browserShutdownFailure || this.pendingTargetPreparations.size > 0) {
5847
+ return;
5848
+ }
5849
+ if ([...this.states.values()].some((state) => !state.disposed && state.dialogs.length > 0)) {
5850
+ return;
5851
+ }
5852
+ if (typeof browser.pages === "function") {
5853
+ const pagesResult = await this.enumerateIdlePages(browser);
5854
+ if (!pagesResult) {
5855
+ return;
5856
+ }
5857
+ const livePages = pagesResult.filter((page) => !isPageClosed(page));
5858
+ for (const page of livePages) {
5859
+ const state = [...this.states.values()].find((candidate) => candidate.page === page);
5860
+ if (!state || state.disposed || state.lifecycleGeneration !== this.lifecycleGeneration || state.configurationPromise || !state.navigationGuardInstalled) {
5861
+ return;
5862
+ }
5863
+ if (state.dialogs.length > 0) {
5864
+ return;
5865
+ }
5866
+ }
5867
+ }
5868
+ if (this.queuedOperations !== 1 || this.activeOperationControllers.size !== 1 || this.pendingTargetPreparations.size > 0) {
5869
+ return;
5870
+ }
5871
+ if (this.lastActivityAt !== observedActivityAt || Date.now() - this.lastActivityAt < this.config.browser.idleTimeoutMs) {
5872
+ return;
5873
+ }
5874
+ await this.closeBrowser();
5875
+ }
5876
+ async enumerateIdlePages(browser) {
5877
+ const pagesPromise = Promise.resolve().then(() => browser.pages());
5878
+ const result = await settleWithTimeout(
5879
+ pagesPromise.then(
5880
+ (pages) => ({ pages }),
5881
+ (error) => ({ error })
5882
+ ),
5883
+ Math.min(this.idleSweepIntervalMs(), SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS)
5884
+ );
5885
+ if (!result || "error" in result || !Array.isArray(result.pages)) {
5886
+ return void 0;
5887
+ }
5888
+ return result.pages;
5889
+ }
5236
5890
  async close() {
5237
5891
  if (this.closePromise) {
5238
5892
  return this.closePromise;
@@ -5253,6 +5907,7 @@ var BrowserService = class {
5253
5907
  if (this.shuttingDown) {
5254
5908
  return { closed: false, owned: false, succeeded: true };
5255
5909
  }
5910
+ this.stopIdleSweep();
5256
5911
  this.shuttingDown = true;
5257
5912
  this.lifecycleGeneration += 1;
5258
5913
  this.shutdownController.abort();
@@ -5263,6 +5918,7 @@ var BrowserService = class {
5263
5918
  const lateConnectionSettled = await settlesWithinTimeout(this.connectionSettlementPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS);
5264
5919
  const interruptedShutdown = this.interruptedBrowserShutdown;
5265
5920
  const interruptedSucceeded = interruptedShutdown ? await settleWithTimeout(interruptedShutdown, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS).catch(() => void 0) === true : true;
5921
+ await settleWithTimeout(this.idleSweepPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS).catch(() => void 0);
5266
5922
  const browserResult = await this.closeBrowser();
5267
5923
  return { ...browserResult, succeeded: browserResult.succeeded && connectionSettled && lateConnectionSettled && interruptedSucceeded };
5268
5924
  }
@@ -5274,21 +5930,25 @@ var BrowserService = class {
5274
5930
  queuedOperations: this.queuedOperations,
5275
5931
  currentPageId: this.currentPageId ?? null,
5276
5932
  recoveryRequired: this.recoveryRequired,
5933
+ idleTimeoutMs: this.config.browser.idleTimeoutMs,
5277
5934
  ...this.benchmarkCounters ? { benchmarkCounters: { ...this.benchmarkCounters } } : {}
5278
5935
  };
5279
5936
  }
5280
5937
  sessionSummary() {
5281
5938
  const status = this.connectionStatus();
5282
- return { session_id: this.sessionId, active: status.connected, owned: status.owned, trackedPages: status.trackedPages, queuedOperations: status.queuedOperations, currentPageId: status.currentPageId, recoveryRequired: this.recoveryRequired, lastActivityAt: new Date(this.lastActivityAt).toISOString() };
5939
+ return { session_id: this.sessionId, active: status.connected, owned: status.owned, trackedPages: status.trackedPages, queuedOperations: status.queuedOperations, currentPageId: status.currentPageId, recoveryRequired: this.recoveryRequired, idleTimeoutMs: this.config.browser.idleTimeoutMs, lastActivityAt: new Date(this.lastActivityAt).toISOString() };
5283
5940
  }
5284
5941
  async doctor() {
5285
5942
  const discovered = this.config.browser.executablePath ? void 0 : findChromeExecutable();
5286
- const executablePath = this.config.browser.executablePath ?? discovered?.path;
5943
+ const configuredExecutablePath = this.config.browser.executablePath;
5944
+ const executablePath = configuredExecutablePath ?? discovered?.path;
5945
+ const executable = configuredExecutablePath ? { source: "configured", ready: isExecutableReady(configuredExecutablePath) } : discovered ? { source: "discovered", ready: isExecutableReady(discovered.path), label: discovered.label, channel: discovered.channel } : { source: "missing", ready: false };
5287
5946
  const endpoint = await this.probeManagedEndpoint();
5288
5947
  const browser = endpoint.version?.Browser;
5289
5948
  return {
5290
5949
  mode: this.config.browser.mode,
5291
5950
  executablePath: executablePath ?? null,
5951
+ executable,
5292
5952
  ...executablePath ? {} : { searchedPaths: chromeExecutableSearchPaths().slice(0, 128) },
5293
5953
  userDataDir: this.config.browser.userDataDir ?? null,
5294
5954
  endpoint: {
@@ -5377,18 +6037,7 @@ var BrowserService = class {
5377
6037
  this.recoveryRequired = !succeeded2;
5378
6038
  return { closed: false, owned: false, succeeded: succeeded2 };
5379
6039
  }
5380
- let succeeded = true;
5381
- if (owned) {
5382
- await Promise.resolve().then(() => browser.close()).catch((error) => {
5383
- succeeded = false;
5384
- this.logger.warn("Browser close failed", { error: String(error) });
5385
- });
5386
- } else {
5387
- await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
5388
- succeeded = false;
5389
- this.logger.warn("Browser disconnect failed", { error: String(error) });
5390
- });
5391
- }
6040
+ const succeeded = await closeConnectedBrowser(browser, owned, this.logger);
5392
6041
  if (!succeeded) {
5393
6042
  this.browserShutdownFailure = true;
5394
6043
  this.failedBrowserShutdown = { browser, owned };
@@ -6326,12 +6975,56 @@ var BrowserService = class {
6326
6975
  return { enabled: false };
6327
6976
  case "get_network_log":
6328
6977
  return { entries: untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES)) };
6978
+ case "search_network_log": {
6979
+ const result = action.query ? this.networkJournal.search(action.query, {
6980
+ pageId: state.id,
6981
+ requestId: action.requestId,
6982
+ url: action.url,
6983
+ method: action.method,
6984
+ status: action.status,
6985
+ resourceType: action.resourceType,
6986
+ offset: action.offset,
6987
+ limit: action.limit
6988
+ }) : this.networkJournal.query({
6989
+ pageId: state.id,
6990
+ requestId: action.requestId,
6991
+ url: action.url,
6992
+ method: action.method,
6993
+ status: action.status,
6994
+ resourceType: action.resourceType,
6995
+ offset: action.offset,
6996
+ limit: action.limit
6997
+ });
6998
+ return {
6999
+ ...result,
7000
+ entries: result.entries.map((entry) => ({
7001
+ ...entry,
7002
+ url: wrapUntrustedText("network_log_url", redactSecretPlaceholders(entry.url), 4096)
7003
+ }))
7004
+ };
7005
+ }
7006
+ case "resource_blocking": {
7007
+ const operation = requireField(action.operation, "operation");
7008
+ if (operation === "set") {
7009
+ const resourceTypes2 = action.resourceTypes ?? [];
7010
+ if (resourceTypes2.length === 0 || new Set(resourceTypes2).size !== resourceTypes2.length || resourceTypes2.some((resourceType) => !RESOURCE_BLOCKING_TYPES.includes(resourceType))) {
7011
+ throw new AppError("INVALID_ACTION", "Resource blocking set requires a non-empty de-duplicated list of supported resourceTypes.");
7012
+ }
7013
+ state.blockedResourceTypes = new Set(resourceTypes2);
7014
+ } else if (operation === "clear") {
7015
+ state.blockedResourceTypes.clear();
7016
+ }
7017
+ const resourceTypes = RESOURCE_BLOCKING_TYPES.filter((resourceType) => state.blockedResourceTypes.has(resourceType));
7018
+ return { pageId: state.id, operation, resourceTypes };
7019
+ }
6329
7020
  case "clear_network_log":
6330
7021
  state.network = [];
7022
+ this.networkJournal.clear(state.id);
6331
7023
  return { cleared: true };
6332
7024
  case "getclear_network_log": {
6333
7025
  const entries = untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES));
6334
7026
  state.network = [];
7027
+ this.networkJournal.clear(state.id);
6335
7028
  return { entries, cleared: true };
6336
7029
  }
6337
7030
  case "enable_console_log":
@@ -6888,27 +7581,53 @@ var BrowserService = class {
6888
7581
  case "upload_file": {
6889
7582
  throwIfAborted(signal);
6890
7583
  const selector = await this.selectorFor(state, targetForAction(action, "selector"), action.frameId);
6891
- throwIfAborted(signal);
6892
- const staged = await this.stageUploadFile(requireField(action.filePath, "filePath"), signal);
6893
- throwIfAborted(signal);
6894
- let input;
7584
+ const stagedFiles = [];
7585
+ let input = null;
6895
7586
  try {
7587
+ if (action.filePath !== void 0 && action.filePaths !== void 0) {
7588
+ throw new AppError("INVALID_ACTION", "Provide filePath or filePaths, not both.");
7589
+ }
7590
+ const rawPaths = action.filePaths ?? (action.filePath !== void 0 ? [action.filePath] : []);
7591
+ if (rawPaths.length === 0 || rawPaths.length > 20) {
7592
+ throw new AppError("INVALID_ACTION", "Upload requires one to 20 paths in filePath or filePaths.");
7593
+ }
7594
+ let totalBytes = 0;
7595
+ for (const rawPath of rawPaths) {
7596
+ throwIfAborted(signal);
7597
+ const staged = await this.stageUploadFile(rawPath, signal);
7598
+ stagedFiles.push(staged);
7599
+ totalBytes += staged.size;
7600
+ if (totalBytes > MAX_UPLOAD_TOTAL_BYTES) {
7601
+ throw new AppError("FILE_TOO_LARGE", "The combined upload sources exceed the 100 MiB size limit.");
7602
+ }
7603
+ }
7604
+ throwIfAborted(signal);
6896
7605
  input = await frame.$(selector);
6897
- } catch (error) {
6898
- await unlinkIfPresent(staged.path);
6899
- throw error;
6900
- }
6901
- if (!input) {
6902
- await unlinkIfPresent(staged.path);
6903
- throw new AppError("ELEMENT_NOT_FOUND", `No element matched '${selector}'.`);
6904
- }
6905
- try {
6906
- await input.uploadFile(staged.path);
7606
+ if (!input) {
7607
+ throw new AppError("ELEMENT_NOT_FOUND", `No element matched '${selector}'.`);
7608
+ }
7609
+ if (stagedFiles.length > 1) {
7610
+ const supportsMultiple = typeof input.evaluate === "function" && await input.evaluate((element) => element instanceof HTMLInputElement && element.type === "file" && element.multiple);
7611
+ if (!supportsMultiple) {
7612
+ throw new AppError("MULTIPLE_FILES_UNSUPPORTED", "Multiple uploads require a file input with the multiple attribute.");
7613
+ }
7614
+ }
7615
+ await input.uploadFile(...stagedFiles.map((staged) => staged.path));
6907
7616
  throwIfAborted(signal);
6908
- return { uploaded: wrapUntrustedText("uploaded_file_name", redactSecretPlaceholders(basename2(staged.displayName)), 512), bytes: Math.min(staged.size, Number.MAX_SAFE_INTEGER) };
7617
+ const names = stagedFiles.map((staged) => wrapUntrustedText("uploaded_file_name", redactSecretPlaceholders(basename2(staged.displayName)), 512));
7618
+ const bytes = Math.min(totalBytes, Number.MAX_SAFE_INTEGER);
7619
+ if (names.length === 1) {
7620
+ return { uploaded: names[0], bytes };
7621
+ }
7622
+ return { uploaded: names, files: names, count: names.length, bytes };
6909
7623
  } finally {
6910
- await input.dispose().catch(() => void 0);
6911
- await unlinkIfPresent(staged.path);
7624
+ try {
7625
+ await input?.dispose();
7626
+ } catch {
7627
+ }
7628
+ for (const staged of stagedFiles) {
7629
+ await unlinkIfPresent(staged.path).catch(() => void 0);
7630
+ }
6912
7631
  }
6913
7632
  }
6914
7633
  case "screenshot": {
@@ -7104,8 +7823,282 @@ var BrowserService = class {
7104
7823
  }, query, { maxNodes: MAX_DOM_TRAVERSAL_NODES, maxChars: MAX_TEXT_SCAN_CHARS });
7105
7824
  return { query, matches: matches.matches.map((match) => wrapUntrustedText("page_match", redactSecretPlaceholders(match), 500)), totalMatches: matches.totalMatches, matchesTruncated: matches.totalMatches > matches.matches.length || matches.scanTruncated };
7106
7825
  }
7826
+ case "inspect_element": {
7827
+ const selector = await this.selectorFor(state, targetForAction(action, "target"), action.frameId, frame);
7828
+ const maxDepth = Number.isSafeInteger(action.maxDepth) ? Math.max(0, Math.min(3, action.maxDepth)) : 1;
7829
+ const maxChildren = Number.isSafeInteger(action.maxChildren) ? Math.max(1, Math.min(100, action.maxChildren)) : 20;
7830
+ const inspected = await frame.$eval(selector, (element, options) => {
7831
+ const excludedTags = /* @__PURE__ */ new Set(["script", "style", "template", "noscript"]);
7832
+ const safeAttributes = new Set(options.safeAttributeNames);
7833
+ const safeDataAttributes = new Set(options.safeDataAttributeNames);
7834
+ const styleNames = [
7835
+ "display",
7836
+ "visibility",
7837
+ "position",
7838
+ "color",
7839
+ "backgroundColor",
7840
+ "width",
7841
+ "height",
7842
+ "zIndex",
7843
+ "fontFamily",
7844
+ "fontSize",
7845
+ "fontWeight",
7846
+ "lineHeight",
7847
+ "opacity",
7848
+ "transform"
7849
+ ];
7850
+ const animationNames = [
7851
+ "animationName",
7852
+ "animationDuration",
7853
+ "animationTimingFunction",
7854
+ "animationDelay",
7855
+ "animationIterationCount",
7856
+ "animationDirection",
7857
+ "animationFillMode",
7858
+ "animationPlayState",
7859
+ "transitionProperty",
7860
+ "transitionDuration",
7861
+ "transitionTimingFunction",
7862
+ "transitionDelay",
7863
+ "transform"
7864
+ ];
7865
+ let visitedNodes = 0;
7866
+ const boundedString = (value, limit = options.maxStringChars) => typeof value === "string" ? value.slice(0, limit) : "";
7867
+ const boundedRect = (target) => {
7868
+ const rect = target.getBoundingClientRect();
7869
+ const bound = (value, minimum = -1e7) => Number.isFinite(value) ? Math.max(minimum, Math.min(1e7, Math.round(value))) : 0;
7870
+ return { x: bound(rect.x), y: bound(rect.y), width: bound(rect.width, 0), height: bound(rect.height, 0) };
7871
+ };
7872
+ const collectAttributes = (target) => {
7873
+ const attributes = {};
7874
+ let omittedAttributes = 0;
7875
+ const attributeCount = target.attributes.length;
7876
+ const inspectedAttributes = Math.min(attributeCount, 40);
7877
+ for (let index = 0; index < inspectedAttributes; index += 1) {
7878
+ const attribute = target.attributes[index];
7879
+ if (!attribute) continue;
7880
+ const name = attribute.name.toLowerCase();
7881
+ const allowed = safeAttributes.has(name) || safeDataAttributes.has(name) || /^aria-[a-z0-9_-]+$/i.test(name);
7882
+ if (!allowed) {
7883
+ omittedAttributes += 1;
7884
+ continue;
7885
+ }
7886
+ const value = boundedString(attribute.value, 200);
7887
+ attributes[name.slice(0, 100)] = value;
7888
+ if (value.length < attribute.value.length) omittedAttributes += 1;
7889
+ }
7890
+ omittedAttributes += Math.max(0, attributeCount - inspectedAttributes);
7891
+ return { attributes, omittedAttributes };
7892
+ };
7893
+ const readSafeText = (target) => {
7894
+ const tag = target.tagName.toLowerCase();
7895
+ if (["input", "textarea", "select", "option"].includes(tag)) {
7896
+ return { text: "", truncated: false };
7897
+ }
7898
+ const stack = [target];
7899
+ let text = "";
7900
+ let truncated = false;
7901
+ let visited = 0;
7902
+ while (stack.length > 0) {
7903
+ const node = stack.pop();
7904
+ if (!node) break;
7905
+ visited += 1;
7906
+ if (visited > options.maxNodes) {
7907
+ truncated = true;
7908
+ break;
7909
+ }
7910
+ if (node.nodeType === 3) {
7911
+ const raw = node.nodeValue ?? "";
7912
+ const remaining = options.maxStringChars - text.length;
7913
+ if (remaining <= 0) {
7914
+ truncated = true;
7915
+ break;
7916
+ }
7917
+ text += raw.slice(0, remaining);
7918
+ if (raw.length > remaining) truncated = true;
7919
+ continue;
7920
+ }
7921
+ if (node.nodeType !== 1) continue;
7922
+ const childElement = node;
7923
+ if (excludedTags.has(childElement.tagName.toLowerCase())) continue;
7924
+ const children = childElement.childNodes;
7925
+ for (let index = children.length - 1; index >= 0; index -= 1) {
7926
+ const child = children[index];
7927
+ if (child) stack.push(child);
7928
+ }
7929
+ }
7930
+ return { text: text.replace(/\s+/g, " ").trim().slice(0, options.maxStringChars), truncated };
7931
+ };
7932
+ const readStyle = (target, pseudo = "") => {
7933
+ let style;
7934
+ try {
7935
+ style = getComputedStyle(target, pseudo);
7936
+ } catch {
7937
+ return {};
7938
+ }
7939
+ const output = {};
7940
+ for (const name of styleNames) {
7941
+ output[name] = boundedString(style[name]);
7942
+ }
7943
+ return output;
7944
+ };
7945
+ const readPseudo = (target, pseudo) => {
7946
+ let style;
7947
+ try {
7948
+ style = getComputedStyle(target, pseudo);
7949
+ } catch {
7950
+ return { content: "", styles: {} };
7951
+ }
7952
+ const styles = {};
7953
+ for (const name of styleNames) {
7954
+ styles[name] = boundedString(style[name]);
7955
+ }
7956
+ return { content: boundedString(style.content), styles };
7957
+ };
7958
+ const readAnimations = (target) => {
7959
+ let style;
7960
+ try {
7961
+ style = getComputedStyle(target);
7962
+ } catch {
7963
+ return {};
7964
+ }
7965
+ const output = {};
7966
+ for (const name of animationNames) {
7967
+ output[name] = boundedString(style[name]);
7968
+ }
7969
+ return output;
7970
+ };
7971
+ const collectChildren = (parent, depth) => {
7972
+ const children = [];
7973
+ let childrenTruncated = false;
7974
+ let omittedChildren = 0;
7975
+ const childElements = Array.from(parent.children);
7976
+ if (depth >= options.maxDepth) {
7977
+ return {
7978
+ children,
7979
+ childrenTruncated: childElements.length > 0,
7980
+ omittedChildren: childElements.length
7981
+ };
7982
+ }
7983
+ for (let index = 0; index < childElements.length; index += 1) {
7984
+ const child = childElements[index];
7985
+ if (!child) continue;
7986
+ if (excludedTags.has(child.tagName.toLowerCase())) {
7987
+ omittedChildren += 1;
7988
+ continue;
7989
+ }
7990
+ if (children.length >= options.maxChildren) {
7991
+ childrenTruncated = true;
7992
+ omittedChildren += childElements.length - index;
7993
+ break;
7994
+ }
7995
+ if (visitedNodes >= options.maxNodes) {
7996
+ childrenTruncated = true;
7997
+ omittedChildren += childElements.length - index;
7998
+ break;
7999
+ }
8000
+ visitedNodes += 1;
8001
+ const childAttributes = collectAttributes(child);
8002
+ const childText = readSafeText(child);
8003
+ const nested = collectChildren(child, depth + 1);
8004
+ children.push({
8005
+ tag: child.tagName.toLowerCase(),
8006
+ rect: boundedRect(child),
8007
+ text: childText.text,
8008
+ textTruncated: childText.truncated,
8009
+ attributes: childAttributes.attributes,
8010
+ omittedAttributes: childAttributes.omittedAttributes,
8011
+ children: nested.children,
8012
+ childrenTruncated: nested.childrenTruncated,
8013
+ omittedChildren: nested.omittedChildren
8014
+ });
8015
+ if (nested.childrenTruncated) childrenTruncated = true;
8016
+ omittedChildren += nested.omittedChildren;
8017
+ }
8018
+ return { children, childrenTruncated, omittedChildren };
8019
+ };
8020
+ const rootAttributes = collectAttributes(element);
8021
+ const rootText = readSafeText(element);
8022
+ const childResult = collectChildren(element, 0);
8023
+ const contentOmitted = excludedTags.has(element.tagName.toLowerCase());
8024
+ return {
8025
+ tag: element.tagName.toLowerCase(),
8026
+ rect: boundedRect(element),
8027
+ text: rootText.text,
8028
+ textTruncated: rootText.truncated,
8029
+ attributes: rootAttributes.attributes,
8030
+ omittedAttributes: rootAttributes.omittedAttributes,
8031
+ computedStyles: readStyle(element),
8032
+ pseudoElements: {
8033
+ before: readPseudo(element, "::before"),
8034
+ after: readPseudo(element, "::after")
8035
+ },
8036
+ animations: readAnimations(element),
8037
+ children: childResult.children,
8038
+ childrenTruncated: childResult.childrenTruncated,
8039
+ omittedChildren: childResult.omittedChildren,
8040
+ contentOmitted,
8041
+ omittedContent: contentOmitted ? 1 : 0,
8042
+ truncated: contentOmitted || rootText.truncated || rootAttributes.omittedAttributes > 0 || childResult.childrenTruncated || childResult.omittedChildren > 0
8043
+ };
8044
+ }, {
8045
+ maxDepth,
8046
+ maxChildren,
8047
+ maxNodes: MAX_INSPECT_NODES,
8048
+ maxStringChars: MAX_INSPECT_STRING_CHARS,
8049
+ safeAttributeNames: [...SAFE_ELEMENT_ATTRIBUTE_NAMES],
8050
+ safeDataAttributeNames: [...SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES]
8051
+ });
8052
+ const wrap = (value, kind, max = MAX_INSPECT_STRING_CHARS) => wrapUntrustedText(kind, redactSecretPlaceholders(value), max);
8053
+ const wrapAttributes = (attributes) => Object.fromEntries(
8054
+ // Attribute names are restricted to the fixed allowlist or the
8055
+ // bounded aria-* grammar in the page callback. Keep those names as
8056
+ // keys for ergonomic parity with find_elements; values are still
8057
+ // untrusted and wrapped below.
8058
+ Object.entries(attributes).map(([name, value]) => [name, wrap(value, "inspect_attribute", 200)])
8059
+ );
8060
+ const wrapChild = (child) => ({
8061
+ tag: wrap(child.tag, "inspect_child_tag", 100),
8062
+ rect: child.rect,
8063
+ text: wrap(child.text, "inspect_child_text"),
8064
+ textTruncated: child.textTruncated,
8065
+ attributes: wrapAttributes(child.attributes),
8066
+ omittedAttributes: child.omittedAttributes,
8067
+ children: child.children.map(wrapChild),
8068
+ childrenTruncated: child.childrenTruncated,
8069
+ omittedChildren: child.omittedChildren
8070
+ });
8071
+ const wrapStyleMap = (styles) => Object.fromEntries(
8072
+ Object.entries(styles).map(([name, value]) => [name, wrap(value, "inspect_style")])
8073
+ );
8074
+ const wrapPseudo = (pseudo) => ({
8075
+ content: wrap(pseudo.content, "inspect_pseudo_content"),
8076
+ styles: wrapStyleMap(pseudo.styles)
8077
+ });
8078
+ return {
8079
+ tag: wrap(inspected.tag, "inspect_tag", 100),
8080
+ selector: wrap(selector, "inspect_selector"),
8081
+ rect: inspected.rect,
8082
+ text: wrap(inspected.text, "inspect_text"),
8083
+ textTruncated: inspected.textTruncated,
8084
+ attributes: wrapAttributes(inspected.attributes),
8085
+ omittedAttributes: inspected.omittedAttributes,
8086
+ computedStyles: wrapStyleMap(inspected.computedStyles),
8087
+ pseudoElements: {
8088
+ before: wrapPseudo(inspected.pseudoElements.before),
8089
+ after: wrapPseudo(inspected.pseudoElements.after)
8090
+ },
8091
+ animations: wrapStyleMap(inspected.animations),
8092
+ children: inspected.children.map(wrapChild),
8093
+ childrenTruncated: inspected.childrenTruncated,
8094
+ omittedChildren: inspected.omittedChildren,
8095
+ contentOmitted: inspected.contentOmitted,
8096
+ omittedContent: inspected.omittedContent,
8097
+ truncated: inspected.truncated
8098
+ };
8099
+ }
7107
8100
  case "find_elements": {
7108
- let collectFindElements2 = function(matches, fallbackSelector) {
8101
+ let collectFindElements2 = function(matches, fallbackSelector, safeAttributeNames, safeDataAttributeNames) {
7109
8102
  const boundedText = (root) => {
7110
8103
  const maybeChildNodes = root.childNodes;
7111
8104
  if (!maybeChildNodes) {
@@ -7179,8 +8172,8 @@ var BrowserService = class {
7179
8172
  const boundedHeight = Number.isFinite(rect.height) ? Math.max(-1e7, Math.min(1e7, Math.round(rect.height))) : 0;
7180
8173
  const attributes = {};
7181
8174
  let omittedAttributes = 0;
7182
- const safeAttributes = /* @__PURE__ */ new Set(["id", "class", "role", "type", "name", "placeholder", "title", "tabindex", "style", "fill", "stroke", "x", "y", "x1", "x2", "y1", "y2", "r", "cx", "cy", "width", "height", "points", "transform", "font-size"]);
7183
- const safeDataAttributes = /* @__PURE__ */ new Set(["data-color", "data-index", "data-sides", "data-result", "data-key", "data-type", "data-item", "data-id", "data-start", "data-end", "data-duration", "data-output", "data-value", "data-position", "data-price"]);
8175
+ const safeAttributes = new Set(safeAttributeNames);
8176
+ const safeDataAttributes = new Set(safeDataAttributeNames);
7184
8177
  const attributeCount = element.attributes.length;
7185
8178
  const inspectedAttributes = Math.min(attributeCount, 40);
7186
8179
  for (let index = 0; index < inspectedAttributes; index += 1) {
@@ -7209,7 +8202,7 @@ var BrowserService = class {
7209
8202
  var collectFindElements = collectFindElements2;
7210
8203
  const selector = targetForAction(action, "selector");
7211
8204
  const safeSelector = await this.selectorFor(state, selector, action.frameId, frame);
7212
- const elements = await frame.$$eval(safeSelector, collectFindElements2, safeSelector);
8205
+ const elements = await frame.$$eval(safeSelector, collectFindElements2, safeSelector, [...SAFE_ELEMENT_ATTRIBUTE_NAMES], [...SAFE_ELEMENT_DATA_ATTRIBUTE_NAMES]);
7213
8206
  return elements.map((element) => ({
7214
8207
  tag: element.tag,
7215
8208
  selector: wrapUntrustedText("element_selector", redactSecretPlaceholders(element.selector), 500),
@@ -7386,7 +8379,8 @@ var BrowserService = class {
7386
8379
  case "solve_challenge":
7387
8380
  return this.solveChallenge(state, action, signal);
7388
8381
  case "get_cookies": {
7389
- const cookies = await page.cookies();
8382
+ const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
8383
+ const cookies = scopedUrl ? await page.cookies(scopedUrl.toString()) : await page.cookies();
7390
8384
  return cookies.slice(0, 200).map((cookie) => ({
7391
8385
  name: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookie.name), 256),
7392
8386
  domain: wrapUntrustedText("cookie_domain", redactSecretPlaceholders(cookie.domain), 512),
@@ -7404,15 +8398,16 @@ var BrowserService = class {
7404
8398
  if (action.cookieDomain) {
7405
8399
  await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
7406
8400
  }
7407
- await page.setCookie({ name: cookieName, value: action.cookieValue ?? action.value ?? "", url: url.toString(), domain: action.cookieDomain, path: action.cookiePath ?? "/", secure: action.cookieSecure, httpOnly: action.cookieHttpOnly });
8401
+ await page.setCookie({ name: cookieName, value: action.cookieValue ?? action.value ?? "", url: url.toString(), domain: action.cookieDomain, path: action.cookiePath ?? "/", secure: action.cookieSecure, httpOnly: action.cookieHttpOnly, sameSite: action.cookieSameSite });
7408
8402
  return { set: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
7409
8403
  }
7410
8404
  case "delete_cookies": {
7411
8405
  const cookieName = requireField(action.cookieName, "cookieName");
8406
+ const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
7412
8407
  if (action.cookieDomain) {
7413
8408
  await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
7414
8409
  }
7415
- await page.deleteCookie({ name: cookieName, domain: action.cookieDomain, path: action.cookiePath ?? "/" });
8410
+ await page.deleteCookie({ name: cookieName, ...scopedUrl ? { url: scopedUrl.toString() } : {}, domain: action.cookieDomain, path: action.cookiePath ?? "/" });
7416
8411
  return { deleted: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
7417
8412
  }
7418
8413
  case "get_storage": {
@@ -7657,6 +8652,7 @@ var BrowserService = class {
7657
8652
  if (!executablePath) {
7658
8653
  throw new AppError("BROWSER_NOT_CONFIGURED", `Managed browser mode could not find Chrome. Checked: ${chromeExecutableSearchPaths().join(", ")}. Install Chrome or set SMOOTH_OPERATOR_BROWSER_EXECUTABLE.`);
7659
8654
  }
8655
+ this.assertExecutableReady(executablePath);
7660
8656
  connection = this.launch({
7661
8657
  headless: this.config.browser.headless,
7662
8658
  executablePath,
@@ -7674,6 +8670,7 @@ var BrowserService = class {
7674
8670
  if (!this.config.browser.executablePath) {
7675
8671
  throw new AppError("BROWSER_NOT_CONFIGURED", "Launch mode requires SMOOTH_OPERATOR_BROWSER_EXECUTABLE.");
7676
8672
  }
8673
+ this.assertExecutableReady(this.config.browser.executablePath);
7677
8674
  ownsBrowser = true;
7678
8675
  connection = this.launch({
7679
8676
  headless: this.config.browser.headless,
@@ -7701,6 +8698,13 @@ var BrowserService = class {
7701
8698
  await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
7702
8699
  throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
7703
8700
  }
8701
+ await this.installTargetGuard(browser);
8702
+ if (this.shuttingDown || generation !== this.lifecycleGeneration || this.shutdownController.signal.aborted) {
8703
+ const lateBrowser = browser;
8704
+ browser = void 0;
8705
+ await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
8706
+ throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
8707
+ }
7704
8708
  this.browser = browser;
7705
8709
  this.ownsBrowser = ownsBrowser;
7706
8710
  const connectedBrowser = browser;
@@ -7715,9 +8719,8 @@ var BrowserService = class {
7715
8719
  this.lifecycleGeneration += 1;
7716
8720
  this.retireAllStates();
7717
8721
  });
7718
- this.installTargetGuard(browser);
7719
8722
  browser.on("targetcreated", (target) => {
7720
- void this.prepareTarget(target);
8723
+ void this.trackTargetPreparation(target);
7721
8724
  });
7722
8725
  return browser;
7723
8726
  } catch (error) {
@@ -7763,6 +8766,17 @@ var BrowserService = class {
7763
8766
  }
7764
8767
  return (await loadPuppeteer()).launch(options);
7765
8768
  }
8769
+ assertExecutableReady(executablePath) {
8770
+ let ready = false;
8771
+ try {
8772
+ ready = this.dependencies.isExecutableReady?.(executablePath) ?? isExecutableReady(executablePath);
8773
+ } catch {
8774
+ ready = false;
8775
+ }
8776
+ if (!ready) {
8777
+ throw new AppError("BROWSER_NOT_CONFIGURED", "Configured browser executable is not ready.", { retryable: true });
8778
+ }
8779
+ }
7766
8780
  async connect(options) {
7767
8781
  if (this.dependencies.connect) {
7768
8782
  return this.dependencies.connect(options);
@@ -7779,7 +8793,9 @@ var BrowserService = class {
7779
8793
  try {
7780
8794
  const info = await lstat(activePortPath);
7781
8795
  if (!info.isFile() || info.size > 4096) {
7782
- this.logger.debug("Managed browser DevTools endpoint file is invalid", { path: activePortPath });
8796
+ this.logger.debug("Managed browser DevTools endpoint file is invalid", {
8797
+ endpointFile: { kind: "devtools-active-port", regular: info.isFile(), bounded: info.size <= 4096 }
8798
+ });
7783
8799
  return { state: "stale-probe-failed" };
7784
8800
  }
7785
8801
  raw = await readFile(activePortPath, "utf8");
@@ -7787,19 +8803,24 @@ var BrowserService = class {
7787
8803
  if (isMissingFile(error)) {
7788
8804
  return { state: "no-file" };
7789
8805
  }
7790
- this.logger.debug("Managed browser DevTools endpoint file could not be read", { path: activePortPath, error: String(error) });
8806
+ this.logger.debug("Managed browser DevTools endpoint file could not be read", {
8807
+ endpointFile: { kind: "devtools-active-port", available: false },
8808
+ error: safeErrorDiagnostic(error)
8809
+ });
7791
8810
  return { state: "stale-probe-failed" };
7792
8811
  }
7793
8812
  const browserURL = parseDevToolsActivePort(raw);
7794
8813
  if (!browserURL) {
7795
- this.logger.debug("Managed browser DevTools endpoint file is malformed", { path: activePortPath });
8814
+ this.logger.debug("Managed browser DevTools endpoint file is malformed", {
8815
+ endpointFile: { kind: "devtools-active-port", available: true, valid: false }
8816
+ });
7796
8817
  return { state: "stale-probe-failed" };
7797
8818
  }
7798
8819
  try {
7799
8820
  const version = await (this.dependencies.probeEndpoint ?? probeDevToolsEndpoint)(browserURL, 2e3);
7800
8821
  return { state: "live", browserURL, version };
7801
8822
  } catch (error) {
7802
- this.logger.debug("Managed browser DevTools endpoint probe failed", { browserURL, error: String(error) });
8823
+ this.logger.debug("Managed browser DevTools endpoint probe failed", { browserURL, error: safeErrorDiagnostic(error) });
7803
8824
  return { state: "stale-probe-failed" };
7804
8825
  }
7805
8826
  }
@@ -7943,14 +8964,18 @@ var BrowserService = class {
7943
8964
  return matches[0]?.id;
7944
8965
  }
7945
8966
  /** Install a Fetch guard at the CDP boundary to policy-check new targets while paused. */
7946
- installTargetGuard(browser) {
8967
+ async installTargetGuard(browser) {
7947
8968
  if (this.targetGuardConnection) {
8969
+ if (this.targetGuardReadinessPromise) {
8970
+ await this.targetGuardReadinessPromise;
8971
+ }
7948
8972
  return;
7949
8973
  }
7950
8974
  const connection = browser._connection;
7951
8975
  if (!connection || typeof connection.on !== "function") {
7952
8976
  this.targetGuardUnavailable = true;
7953
8977
  this.logger.warn("Browser target guard is unavailable; popup actions will be blocked");
8978
+ this.targetGuardReadinessPromise = Promise.resolve();
7954
8979
  return;
7955
8980
  }
7956
8981
  this.targetGuardUnavailable = false;
@@ -7958,6 +8983,7 @@ var BrowserService = class {
7958
8983
  if (typeof targetConnection.isAutoAttached !== "function") {
7959
8984
  this.targetGuardUnavailable = true;
7960
8985
  this.logger.warn("Browser target guard is unavailable; attachment ownership cannot be determined");
8986
+ this.targetGuardReadinessPromise = Promise.resolve();
7961
8987
  return;
7962
8988
  }
7963
8989
  const sessionListener = (value) => {
@@ -7971,6 +8997,10 @@ var BrowserService = class {
7971
8997
  if (!event) {
7972
8998
  return;
7973
8999
  }
9000
+ if (this.handledTargetGuardSessions.has(event.sessionId)) {
9001
+ return;
9002
+ }
9003
+ this.handledTargetGuardSessions.add(event.sessionId);
7974
9004
  const session = this.pendingTargetGuardSessions.get(event.sessionId) ?? getCdpSession(targetConnection, event.sessionId);
7975
9005
  this.pendingTargetGuardSessions.delete(event.sessionId);
7976
9006
  this.pendingTargetGuardInfos.delete(event.sessionId);
@@ -7989,6 +9019,16 @@ var BrowserService = class {
7989
9019
  }
7990
9020
  return;
7991
9021
  }
9022
+ if (!this.targetGuardOriginalEmit || !this.targetGuardWrappedEmit) {
9023
+ this.targetGuardUnavailable = true;
9024
+ this.unguardedTargetSessions.add(event.sessionId);
9025
+ if (targetConnection.send) {
9026
+ void targetConnection.send("Target.closeTarget", { targetId: event.targetInfo.targetId }).catch(() => void 0);
9027
+ }
9028
+ void sendSessionCommand(session, "Page.close").catch(() => void 0);
9029
+ this.logger.warn("New browser target guard cannot safely gate debugger resume");
9030
+ return;
9031
+ }
7992
9032
  if (!session) {
7993
9033
  this.unguardedTargetSessions.add(event.sessionId);
7994
9034
  if (targetConnection.send) {
@@ -7998,7 +9038,7 @@ var BrowserService = class {
7998
9038
  return;
7999
9039
  }
8000
9040
  void this.guardTargetSession(session, event.targetInfo).catch((error) => {
8001
- this.logger.warn("New browser target guard failed", { error: String(error) });
9041
+ this.logger.warn("New browser target guard failed", { error: safeErrorDiagnostic(error) });
8002
9042
  });
8003
9043
  };
8004
9044
  const detachedListener = (value) => {
@@ -8015,12 +9055,14 @@ var BrowserService = class {
8015
9055
  }
8016
9056
  this.pendingTargetGuardSessions.delete(value.sessionId);
8017
9057
  this.pendingTargetGuardInfos.delete(value.sessionId);
9058
+ this.handledTargetGuardSessions.delete(value.sessionId);
8018
9059
  };
8019
9060
  targetConnection.on("sessionattached", sessionListener);
8020
9061
  targetConnection.on("Target.attachedToTarget", rawListener);
8021
9062
  targetConnection.on("Target.detachedFromTarget", detachedListener);
8022
9063
  const originalEmit = targetConnection.emit;
8023
- if (originalEmit) {
9064
+ let emitGuardInstalled = false;
9065
+ if (typeof originalEmit === "function") {
8024
9066
  const wrappedEmit = (event, value) => {
8025
9067
  if (event === "Target.attachedToTarget") {
8026
9068
  rawListener(value);
@@ -8029,8 +9071,12 @@ var BrowserService = class {
8029
9071
  };
8030
9072
  try {
8031
9073
  targetConnection.emit = wrappedEmit;
9074
+ if (targetConnection.emit !== wrappedEmit) {
9075
+ throw new Error("Connection emit wrapper was not installed.");
9076
+ }
8032
9077
  this.targetGuardOriginalEmit = originalEmit;
8033
9078
  this.targetGuardWrappedEmit = wrappedEmit;
9079
+ emitGuardInstalled = true;
8034
9080
  } catch {
8035
9081
  this.targetGuardOriginalEmit = void 0;
8036
9082
  this.targetGuardWrappedEmit = void 0;
@@ -8040,12 +9086,33 @@ var BrowserService = class {
8040
9086
  this.targetGuardConnectionListener = sessionListener;
8041
9087
  this.targetGuardRawConnectionListener = rawListener;
8042
9088
  this.targetGuardDetachedListener = detachedListener;
9089
+ if (!emitGuardInstalled) {
9090
+ this.targetGuardUnavailable = true;
9091
+ this.targetGuardReadinessPromise = Promise.resolve();
9092
+ this.logger.warn("Browser target guard is unavailable; debugger resume cannot be gated");
9093
+ return;
9094
+ }
8043
9095
  if (targetConnection.send) {
8044
- void targetConnection.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }).catch((error) => {
8045
- this.targetGuardUnavailable = true;
8046
- this.logger.warn("Browser target auto-attachment could not be enabled", { error: String(error) });
8047
- });
9096
+ const readiness = Promise.resolve().then(() => targetConnection.send?.("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true })).then(
9097
+ () => {
9098
+ if (this.targetGuardConnection === targetConnection) {
9099
+ this.targetGuardUnavailable = false;
9100
+ }
9101
+ },
9102
+ (error) => {
9103
+ if (this.targetGuardConnection === targetConnection) {
9104
+ this.targetGuardUnavailable = true;
9105
+ this.logger.warn("Browser target auto-attachment could not be enabled", { error: safeErrorDiagnostic(error) });
9106
+ }
9107
+ }
9108
+ ).then(() => void 0);
9109
+ this.targetGuardReadinessPromise = readiness;
9110
+ await readiness;
9111
+ return;
8048
9112
  }
9113
+ this.targetGuardUnavailable = true;
9114
+ this.targetGuardReadinessPromise = Promise.resolve();
9115
+ this.logger.warn("Browser target guard is unavailable; auto-attachment cannot be enabled");
8049
9116
  }
8050
9117
  detachTargetGuard() {
8051
9118
  const connection = this.targetGuardConnection;
@@ -8074,11 +9141,13 @@ var BrowserService = class {
8074
9141
  this.targetGuardConnectionListener = void 0;
8075
9142
  this.targetGuardRawConnectionListener = void 0;
8076
9143
  this.targetGuardDetachedListener = void 0;
9144
+ this.targetGuardReadinessPromise = void 0;
8077
9145
  this.targetGuardOriginalEmit = void 0;
8078
9146
  this.targetGuardWrappedEmit = void 0;
8079
9147
  this.targetGuardUnavailable = false;
8080
9148
  this.pendingTargetGuardSessions.clear();
8081
9149
  this.pendingTargetGuardInfos.clear();
9150
+ this.handledTargetGuardSessions.clear();
8082
9151
  for (const guard of this.targetGuardSessions.values()) {
8083
9152
  guard.released = true;
8084
9153
  removeCdpListener(guard.session, "Fetch.requestPaused", guard.requestPausedListener);
@@ -8108,7 +9177,7 @@ var BrowserService = class {
8108
9177
  };
8109
9178
  guard.requestPausedListener = (event) => {
8110
9179
  const pending = this.handleTargetGuardRequest(guard, event).catch((error) => {
8111
- this.logger.debug("New target request guard callback failed", { error: String(error) });
9180
+ this.logger.debug("New target request guard callback failed", { error: safeErrorDiagnostic(error) });
8112
9181
  });
8113
9182
  guard.pendingRequests.add(pending);
8114
9183
  void pending.finally(() => guard.pendingRequests.delete(pending)).catch(() => void 0);
@@ -8168,7 +9237,7 @@ var BrowserService = class {
8168
9237
  this.targetGuardSessions.delete(sessionId);
8169
9238
  this.unguardedTargetSessions.add(sessionId);
8170
9239
  await this.closeGuardedTarget(guard);
8171
- this.logger.warn("New browser target could not be guarded", { error: String(error) });
9240
+ this.logger.warn("New browser target could not be guarded", { error: safeErrorDiagnostic(error) });
8172
9241
  throw error;
8173
9242
  }
8174
9243
  }
@@ -8225,7 +9294,7 @@ var BrowserService = class {
8225
9294
  await guard.session.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" });
8226
9295
  }
8227
9296
  } catch (error) {
8228
- this.logger.debug("New target request could not be resolved", { error: String(error) });
9297
+ this.logger.debug("New target request could not be resolved", { error: safeErrorDiagnostic(error) });
8229
9298
  } finally {
8230
9299
  guard.requestIds.delete(requestId);
8231
9300
  }
@@ -8304,9 +9373,15 @@ var BrowserService = class {
8304
9373
  throw error;
8305
9374
  }
8306
9375
  } catch (error) {
8307
- this.logger.warn("New browser tab could not be prepared", { error: String(error) });
9376
+ this.logger.warn("New browser tab could not be prepared", { error: safeErrorDiagnostic(error) });
8308
9377
  }
8309
9378
  }
9379
+ trackTargetPreparation(target) {
9380
+ const preparation = this.prepareTarget(target);
9381
+ this.pendingTargetPreparations.add(preparation);
9382
+ void preparation.finally(() => this.pendingTargetPreparations.delete(preparation)).catch(() => void 0);
9383
+ return preparation;
9384
+ }
8310
9385
  isUnguardedTargetPage(page) {
8311
9386
  const identity = pageTargetIdentity(page);
8312
9387
  return Boolean(identity.sessionId && this.unguardedTargetSessions.has(identity.sessionId));
@@ -8361,7 +9436,7 @@ var BrowserService = class {
8361
9436
  state.challengeStatus = void 0;
8362
9437
  state.challengeAttempts = 0;
8363
9438
  } catch (error) {
8364
- this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error: String(error) });
9439
+ this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error: safeErrorDiagnostic(error) });
8365
9440
  }
8366
9441
  }
8367
9442
  async disposePageState(state) {
@@ -8386,6 +9461,7 @@ var BrowserService = class {
8386
9461
  state.snapshotInteractive = void 0;
8387
9462
  state.snapshotId = void 0;
8388
9463
  state.policyVerifiedUrls?.clear();
9464
+ this.networkJournal.clear(state.id);
8389
9465
  state.dialogs.length = 0;
8390
9466
  state.navigationError = void 0;
8391
9467
  state.activeNavigationGeneration = void 0;
@@ -8558,7 +9634,7 @@ var BrowserService = class {
8558
9634
  throwIfAborted(signal);
8559
9635
  const classified = error instanceof AppError && error.code === "DOWNLOAD_CONFIGURATION_FAILED" ? error : new AppError("DOWNLOAD_CONFIGURATION_FAILED", "The browser download directory could not be configured. Retry after reconnecting the browser.", { retryable: true, cause: error });
8560
9636
  state.downloadConfigurationError = classified;
8561
- this.logger.warn("Browser download behavior could not be configured", { pageId: state.id, error: String(error) });
9637
+ this.logger.warn("Browser download behavior could not be configured", { pageId: state.id, error: safeErrorDiagnostic(error) });
8562
9638
  }
8563
9639
  }
8564
9640
  this.assertStateLive(state);
@@ -8638,6 +9714,18 @@ var BrowserService = class {
8638
9714
  state.policyVerifiedUrls?.clear();
8639
9715
  }
8640
9716
  requestUrl = request.url();
9717
+ const resourceType = typeof request.resourceType === "function" ? request.resourceType()?.toLowerCase() : void 0;
9718
+ if (!navigationRequest && resourceType && state.blockedResourceTypes.has(resourceType)) {
9719
+ let handled = true;
9720
+ try {
9721
+ handled = request.isInterceptResolutionHandled();
9722
+ } catch {
9723
+ }
9724
+ if (!handled) {
9725
+ await request.abort("blockedbyclient").catch(() => void 0);
9726
+ }
9727
+ return;
9728
+ }
8641
9729
  if (/^about:blank(?:#.*)?$/i.test(requestUrl)) {
8642
9730
  await request.continue();
8643
9731
  return;
@@ -8681,6 +9769,35 @@ var BrowserService = class {
8681
9769
  }
8682
9770
  }
8683
9771
  }
9772
+ networkRequestId(request) {
9773
+ const existing = this.networkRequestIds.get(request);
9774
+ if (existing) {
9775
+ return existing;
9776
+ }
9777
+ try {
9778
+ const raw = request.id;
9779
+ if (typeof raw === "string" || typeof raw === "number") {
9780
+ return String(raw);
9781
+ }
9782
+ } catch {
9783
+ }
9784
+ return void 0;
9785
+ }
9786
+ networkRequestIdForResponse(pageId, request, url, resourceType, timestamp) {
9787
+ const known = this.networkRequestId(request);
9788
+ if (known) {
9789
+ return known;
9790
+ }
9791
+ const recorded = this.networkJournal.recordRequest({
9792
+ pageId,
9793
+ url,
9794
+ method: "UNKNOWN",
9795
+ ...resourceType ? { resourceType } : {},
9796
+ timestamp
9797
+ });
9798
+ this.networkRequestIds.set(request, recorded.requestId);
9799
+ return recorded.requestId;
9800
+ }
8684
9801
  stateFor(page) {
8685
9802
  const existingId = this.ids.get(page);
8686
9803
  if (existingId) {
@@ -8690,7 +9807,7 @@ var BrowserService = class {
8690
9807
  }
8691
9808
  this.ids.delete(page);
8692
9809
  }
8693
- const state = { id: randomUUID(), page, lifecycleGeneration: this.lifecycleGeneration, disposed: false, refs: /* @__PURE__ */ new Map(), domRevision: 0, networkEnabled: false, consoleEnabled: false, network: [], console: [], dialogs: [], listenersInstalled: false, timeoutsConfigured: false, viewportConfigured: false, downloadConfigured: false, navigationGuardInstalled: false, stealthInjected: false, navigationGeneration: 0, policyVerifiedUrls: /* @__PURE__ */ new Set() };
9810
+ const state = { id: randomUUID(), page, lifecycleGeneration: this.lifecycleGeneration, disposed: false, refs: /* @__PURE__ */ new Map(), domRevision: 0, networkEnabled: false, consoleEnabled: false, network: [], console: [], dialogs: [], listenersInstalled: false, timeoutsConfigured: false, viewportConfigured: false, downloadConfigured: false, navigationGuardInstalled: false, stealthInjected: false, navigationGeneration: 0, policyVerifiedUrls: /* @__PURE__ */ new Set(), blockedResourceTypes: /* @__PURE__ */ new Set() };
8694
9811
  this.ids.set(page, state.id);
8695
9812
  this.states.set(state.id, state);
8696
9813
  this.installListeners(state);
@@ -8706,10 +9823,23 @@ var BrowserService = class {
8706
9823
  return;
8707
9824
  }
8708
9825
  try {
8709
- state.network.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "request", url: sanitizeUrl(request.url()), method: request.method() });
9826
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9827
+ const url = request.url();
9828
+ const method = request.method();
9829
+ const resourceType = request.resourceType?.();
9830
+ const recorded = this.networkJournal.recordRequest({
9831
+ pageId: state.id,
9832
+ requestId: this.networkRequestId(request),
9833
+ url,
9834
+ method,
9835
+ ...typeof resourceType === "string" ? { resourceType } : {},
9836
+ timestamp
9837
+ });
9838
+ this.networkRequestIds.set(request, recorded.requestId);
9839
+ state.network.push({ timestamp, type: "request", url: sanitizeUrl(url), method });
8710
9840
  trimLog(state.network);
8711
9841
  } catch (error) {
8712
- this.logger.debug("Browser request log entry was unavailable after page disposal", { pageId: state.id, error: String(error) });
9842
+ this.logger.debug("Browser request log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
8713
9843
  }
8714
9844
  };
8715
9845
  state.networkRequestListener = networkRequestListener;
@@ -8723,11 +9853,24 @@ var BrowserService = class {
8723
9853
  state.mainFrameStatus = response.status();
8724
9854
  }
8725
9855
  if (state.networkEnabled) {
8726
- state.network.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "response", url: sanitizeUrl(response.url()), status: response.status() });
9856
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
9857
+ const request = response.request();
9858
+ const url = response.url();
9859
+ const resourceType = request.resourceType?.();
9860
+ const requestId = this.networkRequestIdForResponse(state.id, request, url, resourceType, timestamp);
9861
+ this.networkJournal.recordResponse({
9862
+ pageId: state.id,
9863
+ requestId,
9864
+ url,
9865
+ status: response.status(),
9866
+ ...typeof resourceType === "string" ? { resourceType } : {},
9867
+ timestamp
9868
+ });
9869
+ state.network.push({ timestamp, type: "response", url: sanitizeUrl(url), status: response.status() });
8727
9870
  trimLog(state.network);
8728
9871
  }
8729
9872
  } catch (error) {
8730
- this.logger.debug("Browser response log entry was unavailable after page disposal", { pageId: state.id, error: String(error) });
9873
+ this.logger.debug("Browser response log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
8731
9874
  }
8732
9875
  };
8733
9876
  state.networkResponseListener = networkResponseListener;
@@ -8740,7 +9883,7 @@ var BrowserService = class {
8740
9883
  state.console.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "console", level: message.type(), text: message.text().slice(0, 2e3) });
8741
9884
  trimLog(state.console);
8742
9885
  } catch (error) {
8743
- this.logger.debug("Browser console log entry was unavailable after page disposal", { pageId: state.id, error: String(error) });
9886
+ this.logger.debug("Browser console log entry was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
8744
9887
  }
8745
9888
  };
8746
9889
  state.consoleListener = consoleListener;
@@ -8755,7 +9898,7 @@ var BrowserService = class {
8755
9898
  this.currentPageId = state.id;
8756
9899
  this.logger.info("Browser dialog opened", { pageId: state.id, type });
8757
9900
  } catch (error) {
8758
- this.logger.debug("Browser dialog event was unavailable after page disposal", { pageId: state.id, error: String(error) });
9901
+ this.logger.debug("Browser dialog event was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
8759
9902
  }
8760
9903
  };
8761
9904
  state.dialogListener = dialogListener;
@@ -8776,7 +9919,7 @@ var BrowserService = class {
8776
9919
  state.challengeAttempts = 0;
8777
9920
  }
8778
9921
  } catch (error) {
8779
- this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error: String(error) });
9922
+ this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error: safeErrorDiagnostic(error) });
8780
9923
  }
8781
9924
  };
8782
9925
  state.frameNavigatedListener = frameNavigatedListener;
@@ -9436,6 +10579,7 @@ var BrowserService = class {
9436
10579
  try {
9437
10580
  await humanMouseMove(state.page, 0, 0, centerX, centerY, 80);
9438
10581
  } catch {
10582
+ throwIfAborted(signal);
9439
10583
  }
9440
10584
  }
9441
10585
  async clickTarget(state, target, button, clickCount, signal, frame = state.page.mainFrame(), pointerType = "mouse") {
@@ -9800,7 +10944,7 @@ var BrowserService = class {
9800
10944
  removeDialogListener?.();
9801
10945
  if (openedDialog === null) {
9802
10946
  void click.catch((error) => {
9803
- this.logger.debug("Browser click completed after dialog resolution", { pageId: state.id, error: String(error) });
10947
+ this.logger.debug("Browser click completed after dialog resolution", { pageId: state.id, error: safeErrorDiagnostic(error) });
9804
10948
  });
9805
10949
  throwIfAborted(signal);
9806
10950
  return { navigated: false, urlChanged: false };
@@ -9949,7 +11093,7 @@ var BrowserService = class {
9949
11093
  if (!nativeControlValueSet) {
9950
11094
  throwIfAborted(signal);
9951
11095
  if (this.stealthSettings().behaviorEnabled) {
9952
- await humanType(state.page, text);
11096
+ await humanType(state.page, text, { signal });
9953
11097
  } else {
9954
11098
  await state.page.keyboard.type(text);
9955
11099
  }
@@ -10545,6 +11689,9 @@ var BrowserService = class {
10545
11689
  if (before.isSymbolicLink()) {
10546
11690
  throw new AppError("FILE_PATH_BLOCKED", "The upload source must not be a symbolic link.");
10547
11691
  }
11692
+ if (before.size > MAX_UPLOAD_BYTES) {
11693
+ throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
11694
+ }
10548
11695
  const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
10549
11696
  let sourceHandle;
10550
11697
  let stagingPath;
@@ -10554,6 +11701,9 @@ var BrowserService = class {
10554
11701
  if (!opened.isFile()) {
10555
11702
  throw new AppError("FILE_PATH_BLOCKED", "The upload source must be a regular file.");
10556
11703
  }
11704
+ if (opened.size > MAX_UPLOAD_BYTES) {
11705
+ throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
11706
+ }
10557
11707
  const after = await lstat(candidate);
10558
11708
  if (after.isSymbolicLink() || !sameFileIdentity(opened, after)) {
10559
11709
  throw new AppError("FILE_PATH_BLOCKED", "The upload source changed while it was being opened.", { retryable: true });
@@ -10563,10 +11713,15 @@ var BrowserService = class {
10563
11713
  await mkdir(stagingDirectory, { recursive: true, mode: 448 });
10564
11714
  stagingPath = join4(stagingDirectory, `.upload-${randomUUID()}`);
10565
11715
  const stagingHandle = await open(stagingPath, "wx", 384);
11716
+ let copiedBytes = 0;
10566
11717
  try {
10567
11718
  for await (const chunk of sourceHandle.createReadStream({ autoClose: false })) {
10568
11719
  throwIfAborted(signal);
10569
11720
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
11721
+ copiedBytes += buffer.byteLength;
11722
+ if (copiedBytes > MAX_UPLOAD_BYTES) {
11723
+ throw new AppError("FILE_TOO_LARGE", "The upload source exceeds the 50 MiB size limit.");
11724
+ }
10570
11725
  let offset = 0;
10571
11726
  while (offset < buffer.byteLength) {
10572
11727
  const written = await stagingHandle.write(buffer, offset, buffer.byteLength - offset, null);
@@ -10581,7 +11736,7 @@ var BrowserService = class {
10581
11736
  await stagingHandle.close().catch(() => void 0);
10582
11737
  }
10583
11738
  throwIfAborted(signal);
10584
- return { path: stagingPath, displayName: basename2(candidate), size: opened.size };
11739
+ return { path: stagingPath, displayName: basename2(candidate), size: copiedBytes };
10585
11740
  } catch (error) {
10586
11741
  if (stagingPath) {
10587
11742
  await unlinkIfPresent(stagingPath);
@@ -10662,7 +11817,7 @@ var BrowserService = class {
10662
11817
  }).catch(() => void 0);
10663
11818
  return recovery;
10664
11819
  }
10665
- async withOperationLock(signal, operation, queueTimeoutMs = this.config.browser.actionTimeoutMs, operationTimeoutMs, mode = "exclusive") {
11820
+ async withOperationLock(signal, operation, queueTimeoutMs = this.config.browser.actionTimeoutMs, operationTimeoutMs, mode = "exclusive", touchActivity = true) {
10666
11821
  if (this.queuedOperations >= MAX_QUEUED_OPERATIONS) {
10667
11822
  throw new AppError("BROWSER_QUEUE_FULL", "The browser action queue is full; wait for an active operation to finish and retry.", { retryable: true, details: { hint: "Wait for the active browser operation to finish, then retry." } });
10668
11823
  }
@@ -10743,7 +11898,9 @@ var BrowserService = class {
10743
11898
  }
10744
11899
  }
10745
11900
  }, operationBudgetMs);
10746
- this.lastActivityAt = Date.now();
11901
+ if (touchActivity) {
11902
+ this.lastActivityAt = Date.now();
11903
+ }
10747
11904
  operationPromise = Promise.resolve().then(() => operation(operationSignal));
10748
11905
  void operationPromise.catch(() => void 0);
10749
11906
  if (abortRequested) {
@@ -10870,17 +12027,20 @@ function isBrowserConnectTimeout(error) {
10870
12027
  return error instanceof AppError && error.code === "BROWSER_CONNECT_TIMEOUT";
10871
12028
  }
10872
12029
  async function closeConnectedBrowser(browser, owned, logger) {
10873
- let succeeded = true;
10874
- if (owned) {
10875
- await Promise.resolve().then(() => browser.close()).catch((error) => {
10876
- succeeded = false;
10877
- logger.warn("Late browser close failed", { error: String(error) });
10878
- });
10879
- } else {
10880
- await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
10881
- succeeded = false;
10882
- logger.warn("Late browser disconnect failed", { error: String(error) });
10883
- });
12030
+ const closing = Promise.resolve().then(() => owned ? browser.close() : browser.disconnect());
12031
+ const succeeded = await settleWithTimeout(
12032
+ closing.then(
12033
+ () => true,
12034
+ (error) => {
12035
+ logger.warn(owned ? "Browser close failed" : "Browser disconnect failed", { error: safeErrorDiagnostic(error) });
12036
+ return false;
12037
+ }
12038
+ ),
12039
+ SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS
12040
+ );
12041
+ if (succeeded === void 0) {
12042
+ logger.warn(owned ? "Browser close timed out" : "Browser disconnect timed out");
12043
+ return false;
10884
12044
  }
10885
12045
  return succeeded;
10886
12046
  }
@@ -11450,7 +12610,16 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
11450
12610
  if (!response.ok) {
11451
12611
  throw new Error(`DevTools endpoint returned HTTP ${response.status}.`);
11452
12612
  }
11453
- const value = await response.json();
12613
+ const declaredLength = response.headers.get("content-length");
12614
+ if (declaredLength !== null) {
12615
+ const parsedLength = Number(declaredLength);
12616
+ if (Number.isFinite(parsedLength) && parsedLength > MAX_DEVTOOLS_PROBE_RESPONSE_BYTES) {
12617
+ cancelDevToolsProbeBody(response);
12618
+ throw new Error("DevTools endpoint response exceeded the safety limit.");
12619
+ }
12620
+ }
12621
+ const body = await readBoundedDevToolsResponse(response, MAX_DEVTOOLS_PROBE_RESPONSE_BYTES);
12622
+ const value = JSON.parse(body);
11454
12623
  if (!isRecordValue(value)) {
11455
12624
  throw new Error("DevTools endpoint returned an invalid version payload.");
11456
12625
  }
@@ -11463,6 +12632,39 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
11463
12632
  clearTimeout(timer);
11464
12633
  }
11465
12634
  }
12635
+ async function readBoundedDevToolsResponse(response, maxBytes) {
12636
+ if (!response.body) {
12637
+ throw new Error("DevTools endpoint returned an empty response body.");
12638
+ }
12639
+ const reader = response.body.getReader();
12640
+ const chunks = [];
12641
+ let total = 0;
12642
+ try {
12643
+ while (true) {
12644
+ const next = await reader.read();
12645
+ if (next.done) {
12646
+ break;
12647
+ }
12648
+ const value = next.value;
12649
+ if (!(value instanceof Uint8Array) || value.byteLength > maxBytes - total) {
12650
+ void reader.cancel().catch(() => void 0);
12651
+ throw new Error("DevTools endpoint response exceeded the safety limit.");
12652
+ }
12653
+ const chunk = Buffer.from(value);
12654
+ total += chunk.byteLength;
12655
+ chunks.push(chunk);
12656
+ }
12657
+ } finally {
12658
+ reader.releaseLock();
12659
+ }
12660
+ return Buffer.concat(chunks, total).toString("utf8");
12661
+ }
12662
+ function cancelDevToolsProbeBody(response) {
12663
+ try {
12664
+ void response.body?.cancel().catch(() => void 0);
12665
+ } catch {
12666
+ }
12667
+ }
11466
12668
  function boundedEndpointField(value) {
11467
12669
  return typeof value === "string" ? value.slice(0, 4096) : void 0;
11468
12670
  }
@@ -11574,6 +12776,8 @@ var MAX_RESULT_SNIPPET_CHARS = 4e3;
11574
12776
  var MAX_ATTEMPTS = 3;
11575
12777
  var RETRY_BASE_DELAY_MS = 250;
11576
12778
  var RETRY_MAX_DELAY_MS = 2e3;
12779
+ var MAX_CONCURRENT_RESEARCH = 4;
12780
+ var MAX_RESEARCH_QUEUE = 16;
11577
12781
  var ZERO_WIDTH_PATTERN2 = /[\u200B-\u200D\u2060\uFEFF]/g;
11578
12782
  var CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
11579
12783
  var ANTI_BOT_PATTERN = /(?:captcha|challenge|unusual\s+traffic|automated\s+(?:queries|requests)|access\s+denied|temporarily\s+blocked|too\s+many\s+requests)/i;
@@ -11582,6 +12786,70 @@ var RESULT_CLASS_ATTRIBUTE_PATTERN = /\bclass\s*=\s*(["'])([^"']*)\1/i;
11582
12786
  var RESULT_HREF_ATTRIBUTE_PATTERN = /\bhref\s*=\s*(["'])([^"']*)\1/i;
11583
12787
  var NEXT_RESULT_PATTERN = /<a\b[^>]*\bclass\s*=\s*(["'])[^"']*\bresult__a\b[^"']*\1/i;
11584
12788
  var RESULT_SNIPPET_PATTERN = /\bclass\s*=\s*(["'])[^"']*\bresult__snippet\b[^"']*\1[^>]*>([\s\S]*?)<\/[^>]+>/i;
12789
+ var ResearchAdmission = class {
12790
+ active = 0;
12791
+ queue = [];
12792
+ acquire(signal, abortError = cancelledResearchError) {
12793
+ if (signal?.aborted) {
12794
+ return Promise.reject(abortError());
12795
+ }
12796
+ if (this.active < MAX_CONCURRENT_RESEARCH) {
12797
+ this.active += 1;
12798
+ return Promise.resolve(this.createRelease());
12799
+ }
12800
+ if (this.queue.length >= MAX_RESEARCH_QUEUE) {
12801
+ return Promise.reject(new AppError("RESEARCH_BUSY", "The research service is busy; retry later.", {
12802
+ retryable: true,
12803
+ status: 503,
12804
+ details: { classification: "overloaded" }
12805
+ }));
12806
+ }
12807
+ return new Promise((resolve7, reject) => {
12808
+ const waiter = { resolve: resolve7, reject, signal, abortError };
12809
+ const onAbort = () => {
12810
+ const index = this.queue.indexOf(waiter);
12811
+ if (index < 0) {
12812
+ return;
12813
+ }
12814
+ this.queue.splice(index, 1);
12815
+ signal?.removeEventListener("abort", onAbort);
12816
+ reject(abortError());
12817
+ };
12818
+ waiter.onAbort = onAbort;
12819
+ signal?.addEventListener("abort", onAbort, { once: true });
12820
+ this.queue.push(waiter);
12821
+ if (signal?.aborted) {
12822
+ onAbort();
12823
+ }
12824
+ });
12825
+ }
12826
+ createRelease() {
12827
+ let released = false;
12828
+ return () => {
12829
+ if (released) {
12830
+ return;
12831
+ }
12832
+ released = true;
12833
+ this.active -= 1;
12834
+ this.drain();
12835
+ };
12836
+ }
12837
+ drain() {
12838
+ while (this.active < MAX_CONCURRENT_RESEARCH && this.queue.length > 0) {
12839
+ const waiter = this.queue.shift();
12840
+ if (!waiter) {
12841
+ return;
12842
+ }
12843
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
12844
+ if (waiter.signal?.aborted) {
12845
+ waiter.reject(waiter.abortError());
12846
+ continue;
12847
+ }
12848
+ this.active += 1;
12849
+ waiter.resolve(this.createRelease());
12850
+ }
12851
+ }
12852
+ };
11585
12853
  var ResearchService = class {
11586
12854
  constructor(policy, logger) {
11587
12855
  this.policy = policy;
@@ -11589,6 +12857,7 @@ var ResearchService = class {
11589
12857
  }
11590
12858
  policy;
11591
12859
  logger;
12860
+ admission = new ResearchAdmission();
11592
12861
  async research(query, options = {}, signal) {
11593
12862
  if (typeof query !== "string") {
11594
12863
  throw new AppError("RESEARCH_INVALID", "A non-empty research query is required.");
@@ -11627,7 +12896,17 @@ var ResearchService = class {
11627
12896
  }, REQUEST_TIMEOUT_MS);
11628
12897
  const abort = () => controller.abort();
11629
12898
  signal?.addEventListener("abort", abort, { once: true });
12899
+ let release;
11630
12900
  try {
12901
+ const abortError = () => signal?.aborted ? new AppError("CANCELLED", "The research request was cancelled.") : new AppError("RESEARCH_TIMEOUT", `The research request exceeded its ${REQUEST_TIMEOUT_MS / 1e3}-second timeout.`, {
12902
+ retryable: true,
12903
+ details: { classification: "timeout", timeoutMs: REQUEST_TIMEOUT_MS }
12904
+ });
12905
+ release = await this.admission.acquire(controller.signal, abortError);
12906
+ if (controller.signal.aborted) {
12907
+ controller.abort();
12908
+ throw abortError();
12909
+ }
11631
12910
  const url = await awaitWithAbort2(
11632
12911
  this.policy.assertNavigationAllowedAsync(`https://html.duckduckgo.com/html/?q=${encodedQuery}`),
11633
12912
  controller.signal
@@ -11698,9 +12977,13 @@ var ResearchService = class {
11698
12977
  } finally {
11699
12978
  clearTimeout(timeout);
11700
12979
  signal?.removeEventListener("abort", abort);
12980
+ release?.();
11701
12981
  }
11702
12982
  }
11703
12983
  };
12984
+ function cancelledResearchError() {
12985
+ return new AppError("CANCELLED", "The research request was cancelled.");
12986
+ }
11704
12987
  async function fetchWithRetry(url, signal) {
11705
12988
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
11706
12989
  if (signal.aborted) {
@@ -12230,10 +13513,11 @@ var ServerRuntime = class _ServerRuntime {
12230
13513
  mode: this.config.browser.mode,
12231
13514
  configured: managedBrowser || !browserDisabled && (usesExecutable ? Boolean(this.config.browser.executablePath) : Boolean(this.config.browser.wsEndpoint || this.config.browser.url)),
12232
13515
  connection: browserDisabled ? "disabled" : managedBrowser ? "managed" : usesExecutable ? "executable" : this.config.browser.wsEndpoint ? "websocket" : "devtools-http",
12233
- runtime: browserDisabled ? { connected: false, owned: false, trackedPages: 0, queuedOperations: 0, currentPageId: null, recoveryRequired: false } : this.browser.connectionStatus(),
13516
+ runtime: browserDisabled ? { connected: false, owned: false, trackedPages: 0, queuedOperations: 0, currentPageId: null, recoveryRequired: false, idleTimeoutMs: this.config.browser.idleTimeoutMs } : this.browser.connectionStatus(),
12234
13517
  actionTimeoutMs: this.config.browser.actionTimeoutMs,
12235
13518
  connectTimeoutMs: this.config.browser.connectTimeoutMs,
12236
13519
  cdpTimeoutMs: this.config.browser.cdpTimeoutMs,
13520
+ idleTimeoutMs: this.config.browser.idleTimeoutMs,
12237
13521
  maxScreenshotBytes: this.config.browser.maxScreenshotBytes,
12238
13522
  maxHtmlChars: this.config.browser.maxHtmlChars
12239
13523
  },
@@ -12723,6 +14007,7 @@ async function serveHttp(runtime, shutdown) {
12723
14007
  response.on("error", (error) => runtime.logger.error("MCP HTTP response error", safeErrorDiagnostic(error)));
12724
14008
  request.on("error", (error) => runtime.logger.error("MCP HTTP request error", safeErrorDiagnostic(error)));
12725
14009
  if (!accepting) {
14010
+ closeIncompleteRequestAfterResponse(request, response);
12726
14011
  response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
12727
14012
  response.end(HTTP_SHUTTING_DOWN_BODY);
12728
14013
  return;
@@ -12735,11 +14020,13 @@ async function serveHttp(runtime, shutdown) {
12735
14020
  }
12736
14021
  setCorsHeaders(request, response);
12737
14022
  if (!requestPathMatches(request, config.http.path)) {
14023
+ closeIncompleteRequestAfterResponse(request, response);
12738
14024
  response.writeHead(404, { "content-type": "application/json" });
12739
14025
  response.end(HTTP_NOT_FOUND_BODY);
12740
14026
  return;
12741
14027
  }
12742
14028
  if (request.method === "OPTIONS") {
14029
+ closeIncompleteRequestAfterResponse(request, response);
12743
14030
  response.writeHead(204, {
12744
14031
  "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
12745
14032
  "access-control-allow-headers": request.headers["access-control-request-headers"] ?? "authorization, content-type, accept, mcp-protocol-version, mcp-session-id, last-event-id",
@@ -12750,6 +14037,7 @@ async function serveHttp(runtime, shutdown) {
12750
14037
  return;
12751
14038
  }
12752
14039
  if (!authorized(request, expectedAuthDigest)) {
14040
+ closeIncompleteRequestAfterResponse(request, response);
12753
14041
  response.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
12754
14042
  response.end(HTTP_UNAUTHORIZED_BODY);
12755
14043
  return;
@@ -12757,6 +14045,7 @@ async function serveHttp(runtime, shutdown) {
12757
14045
  let streamPool = isPotentialHttpStream(request) ? activeHttpStreams : activeHttpRequests;
12758
14046
  const poolLimit = streamPool === activeHttpStreams ? MAX_HTTP_STREAM_CONCURRENCY : MAX_HTTP_CONCURRENCY;
12759
14047
  if (streamPool.size >= poolLimit) {
14048
+ closeIncompleteRequestAfterResponse(request, response);
12760
14049
  response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
12761
14050
  response.end(HTTP_BUSY_BODY);
12762
14051
  return;