smooth-operator-mcp 3.0.5 → 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.
@@ -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.5";
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
  }
@@ -5078,6 +5241,294 @@ function globMatches(value, glob) {
5078
5241
  return compiled.test(value);
5079
5242
  }
5080
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
+
5081
5532
  // src/server/browser/service.ts
5082
5533
  var puppeteerModulePromise;
5083
5534
  function loadPuppeteer() {
@@ -5092,6 +5543,52 @@ var POPUP_POST_CLICK_SETTLE_TIMEOUT_MS = 300;
5092
5543
  var MAX_DOM_TRAVERSAL_NODES = 2e4;
5093
5544
  var MAX_TEXT_SCAN_CHARS = 5e5;
5094
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
+ ];
5095
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.";
5096
5593
  var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
5097
5594
  var CHALLENGE_MAX_ATTEMPTS = 100;
@@ -5104,6 +5601,11 @@ var NAVIGATION_CLICK_SETTLE_TIMEOUT_MS = 50;
5104
5601
  var NAVIGATION_CLICK_EVENT_TIMEOUT_MS = 250;
5105
5602
  var NAVIGATION_CLICK_READY_TIMEOUT_MS = 250;
5106
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;
5107
5609
  var COMMON_KEY_ALIASES = {
5108
5610
  ALT: "Alt",
5109
5611
  ARROWDOWN: "ArrowDown",
@@ -5190,13 +5692,15 @@ var PARALLEL_READ_ACTIONS = /* @__PURE__ */ new Set([
5190
5692
  "page_next",
5191
5693
  "search_page",
5192
5694
  "find_elements",
5695
+ "inspect_element",
5193
5696
  "list_frames",
5194
5697
  "accessibility_snapshot",
5195
5698
  "get_computed_style",
5196
5699
  "get_page_info",
5197
5700
  "get_cookies",
5198
5701
  "get_storage",
5199
- "list_downloads"
5702
+ "list_downloads",
5703
+ "search_network_log"
5200
5704
  ]);
5201
5705
  var BrowserService = class {
5202
5706
  constructor(config, policy, logger, dependencies = {}) {
@@ -5204,6 +5708,7 @@ var BrowserService = class {
5204
5708
  this.policy = policy;
5205
5709
  this.logger = logger;
5206
5710
  this.dependencies = dependencies;
5711
+ this.startIdleSweep();
5207
5712
  }
5208
5713
  config;
5209
5714
  policy;
@@ -5234,6 +5739,7 @@ var BrowserService = class {
5234
5739
  currentPageId;
5235
5740
  sessionGeneration = 0;
5236
5741
  states = /* @__PURE__ */ new Map();
5742
+ networkJournal = new NetworkJournal();
5237
5743
  configuredDownloadContexts = /* @__PURE__ */ new WeakSet();
5238
5744
  // The download directory is process/session scoped, while page setup is
5239
5745
  // page scoped. Share the mkdir promise across pages so opening a tab does
@@ -5241,9 +5747,11 @@ var BrowserService = class {
5241
5747
  // so a later page can retry after a transient filesystem failure.
5242
5748
  downloadDirectoryPromise;
5243
5749
  ids = /* @__PURE__ */ new WeakMap();
5750
+ networkRequestIds = /* @__PURE__ */ new WeakMap();
5244
5751
  targetGuardSessions = /* @__PURE__ */ new Map();
5245
5752
  targetGuardNavigationErrors = /* @__PURE__ */ new Map();
5246
5753
  unguardedTargetSessions = /* @__PURE__ */ new Set();
5754
+ handledTargetGuardSessions = /* @__PURE__ */ new Set();
5247
5755
  pendingTargetGuardSessions = /* @__PURE__ */ new Map();
5248
5756
  pendingTargetGuardInfos = /* @__PURE__ */ new Map();
5249
5757
  targetGuardUnavailable = false;
@@ -5251,11 +5759,134 @@ var BrowserService = class {
5251
5759
  targetGuardConnectionListener;
5252
5760
  targetGuardRawConnectionListener;
5253
5761
  targetGuardDetachedListener;
5762
+ targetGuardReadinessPromise;
5254
5763
  targetGuardOriginalEmit;
5255
5764
  targetGuardWrappedEmit;
5256
5765
  operationTail = Promise.resolve();
5257
5766
  queuedOperations = 0;
5258
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
+ }
5259
5890
  async close() {
5260
5891
  if (this.closePromise) {
5261
5892
  return this.closePromise;
@@ -5276,6 +5907,7 @@ var BrowserService = class {
5276
5907
  if (this.shuttingDown) {
5277
5908
  return { closed: false, owned: false, succeeded: true };
5278
5909
  }
5910
+ this.stopIdleSweep();
5279
5911
  this.shuttingDown = true;
5280
5912
  this.lifecycleGeneration += 1;
5281
5913
  this.shutdownController.abort();
@@ -5286,6 +5918,7 @@ var BrowserService = class {
5286
5918
  const lateConnectionSettled = await settlesWithinTimeout(this.connectionSettlementPromise, SHUTDOWN_CONNECTION_SETTLE_TIMEOUT_MS);
5287
5919
  const interruptedShutdown = this.interruptedBrowserShutdown;
5288
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);
5289
5922
  const browserResult = await this.closeBrowser();
5290
5923
  return { ...browserResult, succeeded: browserResult.succeeded && connectionSettled && lateConnectionSettled && interruptedSucceeded };
5291
5924
  }
@@ -5297,21 +5930,25 @@ var BrowserService = class {
5297
5930
  queuedOperations: this.queuedOperations,
5298
5931
  currentPageId: this.currentPageId ?? null,
5299
5932
  recoveryRequired: this.recoveryRequired,
5933
+ idleTimeoutMs: this.config.browser.idleTimeoutMs,
5300
5934
  ...this.benchmarkCounters ? { benchmarkCounters: { ...this.benchmarkCounters } } : {}
5301
5935
  };
5302
5936
  }
5303
5937
  sessionSummary() {
5304
5938
  const status = this.connectionStatus();
5305
- 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() };
5306
5940
  }
5307
5941
  async doctor() {
5308
5942
  const discovered = this.config.browser.executablePath ? void 0 : findChromeExecutable();
5309
- 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 };
5310
5946
  const endpoint = await this.probeManagedEndpoint();
5311
5947
  const browser = endpoint.version?.Browser;
5312
5948
  return {
5313
5949
  mode: this.config.browser.mode,
5314
5950
  executablePath: executablePath ?? null,
5951
+ executable,
5315
5952
  ...executablePath ? {} : { searchedPaths: chromeExecutableSearchPaths().slice(0, 128) },
5316
5953
  userDataDir: this.config.browser.userDataDir ?? null,
5317
5954
  endpoint: {
@@ -5400,18 +6037,7 @@ var BrowserService = class {
5400
6037
  this.recoveryRequired = !succeeded2;
5401
6038
  return { closed: false, owned: false, succeeded: succeeded2 };
5402
6039
  }
5403
- let succeeded = true;
5404
- if (owned) {
5405
- await Promise.resolve().then(() => browser.close()).catch((error) => {
5406
- succeeded = false;
5407
- this.logger.warn("Browser close failed", { error: String(error) });
5408
- });
5409
- } else {
5410
- await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
5411
- succeeded = false;
5412
- this.logger.warn("Browser disconnect failed", { error: String(error) });
5413
- });
5414
- }
6040
+ const succeeded = await closeConnectedBrowser(browser, owned, this.logger);
5415
6041
  if (!succeeded) {
5416
6042
  this.browserShutdownFailure = true;
5417
6043
  this.failedBrowserShutdown = { browser, owned };
@@ -6349,12 +6975,56 @@ var BrowserService = class {
6349
6975
  return { enabled: false };
6350
6976
  case "get_network_log":
6351
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
+ }
6352
7020
  case "clear_network_log":
6353
7021
  state.network = [];
7022
+ this.networkJournal.clear(state.id);
6354
7023
  return { cleared: true };
6355
7024
  case "getclear_network_log": {
6356
7025
  const entries = untrustedLogEntries(state.network.slice(-MAX_LOG_ENTRIES));
6357
7026
  state.network = [];
7027
+ this.networkJournal.clear(state.id);
6358
7028
  return { entries, cleared: true };
6359
7029
  }
6360
7030
  case "enable_console_log":
@@ -6911,27 +7581,53 @@ var BrowserService = class {
6911
7581
  case "upload_file": {
6912
7582
  throwIfAborted(signal);
6913
7583
  const selector = await this.selectorFor(state, targetForAction(action, "selector"), action.frameId);
6914
- throwIfAborted(signal);
6915
- const staged = await this.stageUploadFile(requireField(action.filePath, "filePath"), signal);
6916
- throwIfAborted(signal);
6917
- let input;
7584
+ const stagedFiles = [];
7585
+ let input = null;
6918
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);
6919
7605
  input = await frame.$(selector);
6920
- } catch (error) {
6921
- await unlinkIfPresent(staged.path);
6922
- throw error;
6923
- }
6924
- if (!input) {
6925
- await unlinkIfPresent(staged.path);
6926
- throw new AppError("ELEMENT_NOT_FOUND", `No element matched '${selector}'.`);
6927
- }
6928
- try {
6929
- 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));
6930
7616
  throwIfAborted(signal);
6931
- 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 };
6932
7623
  } finally {
6933
- await input.dispose().catch(() => void 0);
6934
- 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
+ }
6935
7631
  }
6936
7632
  }
6937
7633
  case "screenshot": {
@@ -7127,8 +7823,282 @@ var BrowserService = class {
7127
7823
  }, query, { maxNodes: MAX_DOM_TRAVERSAL_NODES, maxChars: MAX_TEXT_SCAN_CHARS });
7128
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 };
7129
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
+ }
7130
8100
  case "find_elements": {
7131
- let collectFindElements2 = function(matches, fallbackSelector) {
8101
+ let collectFindElements2 = function(matches, fallbackSelector, safeAttributeNames, safeDataAttributeNames) {
7132
8102
  const boundedText = (root) => {
7133
8103
  const maybeChildNodes = root.childNodes;
7134
8104
  if (!maybeChildNodes) {
@@ -7202,8 +8172,8 @@ var BrowserService = class {
7202
8172
  const boundedHeight = Number.isFinite(rect.height) ? Math.max(-1e7, Math.min(1e7, Math.round(rect.height))) : 0;
7203
8173
  const attributes = {};
7204
8174
  let omittedAttributes = 0;
7205
- 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"]);
7206
- 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);
7207
8177
  const attributeCount = element.attributes.length;
7208
8178
  const inspectedAttributes = Math.min(attributeCount, 40);
7209
8179
  for (let index = 0; index < inspectedAttributes; index += 1) {
@@ -7232,7 +8202,7 @@ var BrowserService = class {
7232
8202
  var collectFindElements = collectFindElements2;
7233
8203
  const selector = targetForAction(action, "selector");
7234
8204
  const safeSelector = await this.selectorFor(state, selector, action.frameId, frame);
7235
- 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]);
7236
8206
  return elements.map((element) => ({
7237
8207
  tag: element.tag,
7238
8208
  selector: wrapUntrustedText("element_selector", redactSecretPlaceholders(element.selector), 500),
@@ -7409,7 +8379,8 @@ var BrowserService = class {
7409
8379
  case "solve_challenge":
7410
8380
  return this.solveChallenge(state, action, signal);
7411
8381
  case "get_cookies": {
7412
- 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();
7413
8384
  return cookies.slice(0, 200).map((cookie) => ({
7414
8385
  name: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookie.name), 256),
7415
8386
  domain: wrapUntrustedText("cookie_domain", redactSecretPlaceholders(cookie.domain), 512),
@@ -7427,15 +8398,16 @@ var BrowserService = class {
7427
8398
  if (action.cookieDomain) {
7428
8399
  await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
7429
8400
  }
7430
- 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 });
7431
8402
  return { set: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
7432
8403
  }
7433
8404
  case "delete_cookies": {
7434
8405
  const cookieName = requireField(action.cookieName, "cookieName");
8406
+ const scopedUrl = action.url ? await this.policy.assertNavigationAllowedAsync(action.url) : void 0;
7435
8407
  if (action.cookieDomain) {
7436
8408
  await this.policy.assertNavigationAllowedAsync(`https://${action.cookieDomain.replace(/^\.+/, "")}`);
7437
8409
  }
7438
- 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 ?? "/" });
7439
8411
  return { deleted: wrapUntrustedText("cookie_name", redactSecretPlaceholders(cookieName), 256) };
7440
8412
  }
7441
8413
  case "get_storage": {
@@ -7680,6 +8652,7 @@ var BrowserService = class {
7680
8652
  if (!executablePath) {
7681
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.`);
7682
8654
  }
8655
+ this.assertExecutableReady(executablePath);
7683
8656
  connection = this.launch({
7684
8657
  headless: this.config.browser.headless,
7685
8658
  executablePath,
@@ -7697,6 +8670,7 @@ var BrowserService = class {
7697
8670
  if (!this.config.browser.executablePath) {
7698
8671
  throw new AppError("BROWSER_NOT_CONFIGURED", "Launch mode requires SMOOTH_OPERATOR_BROWSER_EXECUTABLE.");
7699
8672
  }
8673
+ this.assertExecutableReady(this.config.browser.executablePath);
7700
8674
  ownsBrowser = true;
7701
8675
  connection = this.launch({
7702
8676
  headless: this.config.browser.headless,
@@ -7724,6 +8698,13 @@ var BrowserService = class {
7724
8698
  await closeConnectedBrowser(lateBrowser, ownsBrowser, this.logger);
7725
8699
  throw new AppError("SERVER_CLOSING", "The browser runtime is shutting down.", { retryable: true });
7726
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
+ }
7727
8708
  this.browser = browser;
7728
8709
  this.ownsBrowser = ownsBrowser;
7729
8710
  const connectedBrowser = browser;
@@ -7738,9 +8719,8 @@ var BrowserService = class {
7738
8719
  this.lifecycleGeneration += 1;
7739
8720
  this.retireAllStates();
7740
8721
  });
7741
- this.installTargetGuard(browser);
7742
8722
  browser.on("targetcreated", (target) => {
7743
- void this.prepareTarget(target);
8723
+ void this.trackTargetPreparation(target);
7744
8724
  });
7745
8725
  return browser;
7746
8726
  } catch (error) {
@@ -7786,6 +8766,17 @@ var BrowserService = class {
7786
8766
  }
7787
8767
  return (await loadPuppeteer()).launch(options);
7788
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
+ }
7789
8780
  async connect(options) {
7790
8781
  if (this.dependencies.connect) {
7791
8782
  return this.dependencies.connect(options);
@@ -7802,7 +8793,9 @@ var BrowserService = class {
7802
8793
  try {
7803
8794
  const info = await lstat(activePortPath);
7804
8795
  if (!info.isFile() || info.size > 4096) {
7805
- 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
+ });
7806
8799
  return { state: "stale-probe-failed" };
7807
8800
  }
7808
8801
  raw = await readFile(activePortPath, "utf8");
@@ -7810,19 +8803,24 @@ var BrowserService = class {
7810
8803
  if (isMissingFile(error)) {
7811
8804
  return { state: "no-file" };
7812
8805
  }
7813
- 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
+ });
7814
8810
  return { state: "stale-probe-failed" };
7815
8811
  }
7816
8812
  const browserURL = parseDevToolsActivePort(raw);
7817
8813
  if (!browserURL) {
7818
- 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
+ });
7819
8817
  return { state: "stale-probe-failed" };
7820
8818
  }
7821
8819
  try {
7822
8820
  const version = await (this.dependencies.probeEndpoint ?? probeDevToolsEndpoint)(browserURL, 2e3);
7823
8821
  return { state: "live", browserURL, version };
7824
8822
  } catch (error) {
7825
- 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) });
7826
8824
  return { state: "stale-probe-failed" };
7827
8825
  }
7828
8826
  }
@@ -7966,14 +8964,18 @@ var BrowserService = class {
7966
8964
  return matches[0]?.id;
7967
8965
  }
7968
8966
  /** Install a Fetch guard at the CDP boundary to policy-check new targets while paused. */
7969
- installTargetGuard(browser) {
8967
+ async installTargetGuard(browser) {
7970
8968
  if (this.targetGuardConnection) {
8969
+ if (this.targetGuardReadinessPromise) {
8970
+ await this.targetGuardReadinessPromise;
8971
+ }
7971
8972
  return;
7972
8973
  }
7973
8974
  const connection = browser._connection;
7974
8975
  if (!connection || typeof connection.on !== "function") {
7975
8976
  this.targetGuardUnavailable = true;
7976
8977
  this.logger.warn("Browser target guard is unavailable; popup actions will be blocked");
8978
+ this.targetGuardReadinessPromise = Promise.resolve();
7977
8979
  return;
7978
8980
  }
7979
8981
  this.targetGuardUnavailable = false;
@@ -7981,6 +8983,7 @@ var BrowserService = class {
7981
8983
  if (typeof targetConnection.isAutoAttached !== "function") {
7982
8984
  this.targetGuardUnavailable = true;
7983
8985
  this.logger.warn("Browser target guard is unavailable; attachment ownership cannot be determined");
8986
+ this.targetGuardReadinessPromise = Promise.resolve();
7984
8987
  return;
7985
8988
  }
7986
8989
  const sessionListener = (value) => {
@@ -7994,6 +8997,10 @@ var BrowserService = class {
7994
8997
  if (!event) {
7995
8998
  return;
7996
8999
  }
9000
+ if (this.handledTargetGuardSessions.has(event.sessionId)) {
9001
+ return;
9002
+ }
9003
+ this.handledTargetGuardSessions.add(event.sessionId);
7997
9004
  const session = this.pendingTargetGuardSessions.get(event.sessionId) ?? getCdpSession(targetConnection, event.sessionId);
7998
9005
  this.pendingTargetGuardSessions.delete(event.sessionId);
7999
9006
  this.pendingTargetGuardInfos.delete(event.sessionId);
@@ -8012,6 +9019,16 @@ var BrowserService = class {
8012
9019
  }
8013
9020
  return;
8014
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
+ }
8015
9032
  if (!session) {
8016
9033
  this.unguardedTargetSessions.add(event.sessionId);
8017
9034
  if (targetConnection.send) {
@@ -8021,7 +9038,7 @@ var BrowserService = class {
8021
9038
  return;
8022
9039
  }
8023
9040
  void this.guardTargetSession(session, event.targetInfo).catch((error) => {
8024
- this.logger.warn("New browser target guard failed", { error: String(error) });
9041
+ this.logger.warn("New browser target guard failed", { error: safeErrorDiagnostic(error) });
8025
9042
  });
8026
9043
  };
8027
9044
  const detachedListener = (value) => {
@@ -8038,12 +9055,14 @@ var BrowserService = class {
8038
9055
  }
8039
9056
  this.pendingTargetGuardSessions.delete(value.sessionId);
8040
9057
  this.pendingTargetGuardInfos.delete(value.sessionId);
9058
+ this.handledTargetGuardSessions.delete(value.sessionId);
8041
9059
  };
8042
9060
  targetConnection.on("sessionattached", sessionListener);
8043
9061
  targetConnection.on("Target.attachedToTarget", rawListener);
8044
9062
  targetConnection.on("Target.detachedFromTarget", detachedListener);
8045
9063
  const originalEmit = targetConnection.emit;
8046
- if (originalEmit) {
9064
+ let emitGuardInstalled = false;
9065
+ if (typeof originalEmit === "function") {
8047
9066
  const wrappedEmit = (event, value) => {
8048
9067
  if (event === "Target.attachedToTarget") {
8049
9068
  rawListener(value);
@@ -8052,8 +9071,12 @@ var BrowserService = class {
8052
9071
  };
8053
9072
  try {
8054
9073
  targetConnection.emit = wrappedEmit;
9074
+ if (targetConnection.emit !== wrappedEmit) {
9075
+ throw new Error("Connection emit wrapper was not installed.");
9076
+ }
8055
9077
  this.targetGuardOriginalEmit = originalEmit;
8056
9078
  this.targetGuardWrappedEmit = wrappedEmit;
9079
+ emitGuardInstalled = true;
8057
9080
  } catch {
8058
9081
  this.targetGuardOriginalEmit = void 0;
8059
9082
  this.targetGuardWrappedEmit = void 0;
@@ -8063,12 +9086,33 @@ var BrowserService = class {
8063
9086
  this.targetGuardConnectionListener = sessionListener;
8064
9087
  this.targetGuardRawConnectionListener = rawListener;
8065
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
+ }
8066
9095
  if (targetConnection.send) {
8067
- void targetConnection.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }).catch((error) => {
8068
- this.targetGuardUnavailable = true;
8069
- this.logger.warn("Browser target auto-attachment could not be enabled", { error: String(error) });
8070
- });
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;
8071
9112
  }
9113
+ this.targetGuardUnavailable = true;
9114
+ this.targetGuardReadinessPromise = Promise.resolve();
9115
+ this.logger.warn("Browser target guard is unavailable; auto-attachment cannot be enabled");
8072
9116
  }
8073
9117
  detachTargetGuard() {
8074
9118
  const connection = this.targetGuardConnection;
@@ -8097,11 +9141,13 @@ var BrowserService = class {
8097
9141
  this.targetGuardConnectionListener = void 0;
8098
9142
  this.targetGuardRawConnectionListener = void 0;
8099
9143
  this.targetGuardDetachedListener = void 0;
9144
+ this.targetGuardReadinessPromise = void 0;
8100
9145
  this.targetGuardOriginalEmit = void 0;
8101
9146
  this.targetGuardWrappedEmit = void 0;
8102
9147
  this.targetGuardUnavailable = false;
8103
9148
  this.pendingTargetGuardSessions.clear();
8104
9149
  this.pendingTargetGuardInfos.clear();
9150
+ this.handledTargetGuardSessions.clear();
8105
9151
  for (const guard of this.targetGuardSessions.values()) {
8106
9152
  guard.released = true;
8107
9153
  removeCdpListener(guard.session, "Fetch.requestPaused", guard.requestPausedListener);
@@ -8131,7 +9177,7 @@ var BrowserService = class {
8131
9177
  };
8132
9178
  guard.requestPausedListener = (event) => {
8133
9179
  const pending = this.handleTargetGuardRequest(guard, event).catch((error) => {
8134
- 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) });
8135
9181
  });
8136
9182
  guard.pendingRequests.add(pending);
8137
9183
  void pending.finally(() => guard.pendingRequests.delete(pending)).catch(() => void 0);
@@ -8191,7 +9237,7 @@ var BrowserService = class {
8191
9237
  this.targetGuardSessions.delete(sessionId);
8192
9238
  this.unguardedTargetSessions.add(sessionId);
8193
9239
  await this.closeGuardedTarget(guard);
8194
- 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) });
8195
9241
  throw error;
8196
9242
  }
8197
9243
  }
@@ -8248,7 +9294,7 @@ var BrowserService = class {
8248
9294
  await guard.session.send("Fetch.failRequest", { requestId, errorReason: "BlockedByClient" });
8249
9295
  }
8250
9296
  } catch (error) {
8251
- 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) });
8252
9298
  } finally {
8253
9299
  guard.requestIds.delete(requestId);
8254
9300
  }
@@ -8327,9 +9373,15 @@ var BrowserService = class {
8327
9373
  throw error;
8328
9374
  }
8329
9375
  } catch (error) {
8330
- 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) });
8331
9377
  }
8332
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
+ }
8333
9385
  isUnguardedTargetPage(page) {
8334
9386
  const identity = pageTargetIdentity(page);
8335
9387
  return Boolean(identity.sessionId && this.unguardedTargetSessions.has(identity.sessionId));
@@ -8384,7 +9436,7 @@ var BrowserService = class {
8384
9436
  state.challengeStatus = void 0;
8385
9437
  state.challengeAttempts = 0;
8386
9438
  } catch (error) {
8387
- 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) });
8388
9440
  }
8389
9441
  }
8390
9442
  async disposePageState(state) {
@@ -8409,6 +9461,7 @@ var BrowserService = class {
8409
9461
  state.snapshotInteractive = void 0;
8410
9462
  state.snapshotId = void 0;
8411
9463
  state.policyVerifiedUrls?.clear();
9464
+ this.networkJournal.clear(state.id);
8412
9465
  state.dialogs.length = 0;
8413
9466
  state.navigationError = void 0;
8414
9467
  state.activeNavigationGeneration = void 0;
@@ -8581,7 +9634,7 @@ var BrowserService = class {
8581
9634
  throwIfAborted(signal);
8582
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 });
8583
9636
  state.downloadConfigurationError = classified;
8584
- 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) });
8585
9638
  }
8586
9639
  }
8587
9640
  this.assertStateLive(state);
@@ -8661,6 +9714,18 @@ var BrowserService = class {
8661
9714
  state.policyVerifiedUrls?.clear();
8662
9715
  }
8663
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
+ }
8664
9729
  if (/^about:blank(?:#.*)?$/i.test(requestUrl)) {
8665
9730
  await request.continue();
8666
9731
  return;
@@ -8704,6 +9769,35 @@ var BrowserService = class {
8704
9769
  }
8705
9770
  }
8706
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
+ }
8707
9801
  stateFor(page) {
8708
9802
  const existingId = this.ids.get(page);
8709
9803
  if (existingId) {
@@ -8713,7 +9807,7 @@ var BrowserService = class {
8713
9807
  }
8714
9808
  this.ids.delete(page);
8715
9809
  }
8716
- 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() };
8717
9811
  this.ids.set(page, state.id);
8718
9812
  this.states.set(state.id, state);
8719
9813
  this.installListeners(state);
@@ -8729,10 +9823,23 @@ var BrowserService = class {
8729
9823
  return;
8730
9824
  }
8731
9825
  try {
8732
- 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 });
8733
9840
  trimLog(state.network);
8734
9841
  } catch (error) {
8735
- 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) });
8736
9843
  }
8737
9844
  };
8738
9845
  state.networkRequestListener = networkRequestListener;
@@ -8746,11 +9853,24 @@ var BrowserService = class {
8746
9853
  state.mainFrameStatus = response.status();
8747
9854
  }
8748
9855
  if (state.networkEnabled) {
8749
- 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() });
8750
9870
  trimLog(state.network);
8751
9871
  }
8752
9872
  } catch (error) {
8753
- 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) });
8754
9874
  }
8755
9875
  };
8756
9876
  state.networkResponseListener = networkResponseListener;
@@ -8763,7 +9883,7 @@ var BrowserService = class {
8763
9883
  state.console.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), type: "console", level: message.type(), text: message.text().slice(0, 2e3) });
8764
9884
  trimLog(state.console);
8765
9885
  } catch (error) {
8766
- 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) });
8767
9887
  }
8768
9888
  };
8769
9889
  state.consoleListener = consoleListener;
@@ -8778,7 +9898,7 @@ var BrowserService = class {
8778
9898
  this.currentPageId = state.id;
8779
9899
  this.logger.info("Browser dialog opened", { pageId: state.id, type });
8780
9900
  } catch (error) {
8781
- 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) });
8782
9902
  }
8783
9903
  };
8784
9904
  state.dialogListener = dialogListener;
@@ -8799,7 +9919,7 @@ var BrowserService = class {
8799
9919
  state.challengeAttempts = 0;
8800
9920
  }
8801
9921
  } catch (error) {
8802
- 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) });
8803
9923
  }
8804
9924
  };
8805
9925
  state.frameNavigatedListener = frameNavigatedListener;
@@ -9824,7 +10944,7 @@ var BrowserService = class {
9824
10944
  removeDialogListener?.();
9825
10945
  if (openedDialog === null) {
9826
10946
  void click.catch((error) => {
9827
- 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) });
9828
10948
  });
9829
10949
  throwIfAborted(signal);
9830
10950
  return { navigated: false, urlChanged: false };
@@ -10569,6 +11689,9 @@ var BrowserService = class {
10569
11689
  if (before.isSymbolicLink()) {
10570
11690
  throw new AppError("FILE_PATH_BLOCKED", "The upload source must not be a symbolic link.");
10571
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
+ }
10572
11695
  const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
10573
11696
  let sourceHandle;
10574
11697
  let stagingPath;
@@ -10578,6 +11701,9 @@ var BrowserService = class {
10578
11701
  if (!opened.isFile()) {
10579
11702
  throw new AppError("FILE_PATH_BLOCKED", "The upload source must be a regular file.");
10580
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
+ }
10581
11707
  const after = await lstat(candidate);
10582
11708
  if (after.isSymbolicLink() || !sameFileIdentity(opened, after)) {
10583
11709
  throw new AppError("FILE_PATH_BLOCKED", "The upload source changed while it was being opened.", { retryable: true });
@@ -10587,10 +11713,15 @@ var BrowserService = class {
10587
11713
  await mkdir(stagingDirectory, { recursive: true, mode: 448 });
10588
11714
  stagingPath = join4(stagingDirectory, `.upload-${randomUUID()}`);
10589
11715
  const stagingHandle = await open(stagingPath, "wx", 384);
11716
+ let copiedBytes = 0;
10590
11717
  try {
10591
11718
  for await (const chunk of sourceHandle.createReadStream({ autoClose: false })) {
10592
11719
  throwIfAborted(signal);
10593
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
+ }
10594
11725
  let offset = 0;
10595
11726
  while (offset < buffer.byteLength) {
10596
11727
  const written = await stagingHandle.write(buffer, offset, buffer.byteLength - offset, null);
@@ -10605,7 +11736,7 @@ var BrowserService = class {
10605
11736
  await stagingHandle.close().catch(() => void 0);
10606
11737
  }
10607
11738
  throwIfAborted(signal);
10608
- return { path: stagingPath, displayName: basename2(candidate), size: opened.size };
11739
+ return { path: stagingPath, displayName: basename2(candidate), size: copiedBytes };
10609
11740
  } catch (error) {
10610
11741
  if (stagingPath) {
10611
11742
  await unlinkIfPresent(stagingPath);
@@ -10686,7 +11817,7 @@ var BrowserService = class {
10686
11817
  }).catch(() => void 0);
10687
11818
  return recovery;
10688
11819
  }
10689
- 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) {
10690
11821
  if (this.queuedOperations >= MAX_QUEUED_OPERATIONS) {
10691
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." } });
10692
11823
  }
@@ -10767,7 +11898,9 @@ var BrowserService = class {
10767
11898
  }
10768
11899
  }
10769
11900
  }, operationBudgetMs);
10770
- this.lastActivityAt = Date.now();
11901
+ if (touchActivity) {
11902
+ this.lastActivityAt = Date.now();
11903
+ }
10771
11904
  operationPromise = Promise.resolve().then(() => operation(operationSignal));
10772
11905
  void operationPromise.catch(() => void 0);
10773
11906
  if (abortRequested) {
@@ -10894,17 +12027,20 @@ function isBrowserConnectTimeout(error) {
10894
12027
  return error instanceof AppError && error.code === "BROWSER_CONNECT_TIMEOUT";
10895
12028
  }
10896
12029
  async function closeConnectedBrowser(browser, owned, logger) {
10897
- let succeeded = true;
10898
- if (owned) {
10899
- await Promise.resolve().then(() => browser.close()).catch((error) => {
10900
- succeeded = false;
10901
- logger.warn("Late browser close failed", { error: String(error) });
10902
- });
10903
- } else {
10904
- await Promise.resolve().then(() => browser.disconnect()).catch((error) => {
10905
- succeeded = false;
10906
- logger.warn("Late browser disconnect failed", { error: String(error) });
10907
- });
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;
10908
12044
  }
10909
12045
  return succeeded;
10910
12046
  }
@@ -11474,7 +12610,16 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
11474
12610
  if (!response.ok) {
11475
12611
  throw new Error(`DevTools endpoint returned HTTP ${response.status}.`);
11476
12612
  }
11477
- 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);
11478
12623
  if (!isRecordValue(value)) {
11479
12624
  throw new Error("DevTools endpoint returned an invalid version payload.");
11480
12625
  }
@@ -11487,6 +12632,39 @@ async function probeDevToolsEndpoint(browserURL, timeoutMs) {
11487
12632
  clearTimeout(timer);
11488
12633
  }
11489
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
+ }
11490
12668
  function boundedEndpointField(value) {
11491
12669
  return typeof value === "string" ? value.slice(0, 4096) : void 0;
11492
12670
  }
@@ -11598,6 +12776,8 @@ var MAX_RESULT_SNIPPET_CHARS = 4e3;
11598
12776
  var MAX_ATTEMPTS = 3;
11599
12777
  var RETRY_BASE_DELAY_MS = 250;
11600
12778
  var RETRY_MAX_DELAY_MS = 2e3;
12779
+ var MAX_CONCURRENT_RESEARCH = 4;
12780
+ var MAX_RESEARCH_QUEUE = 16;
11601
12781
  var ZERO_WIDTH_PATTERN2 = /[\u200B-\u200D\u2060\uFEFF]/g;
11602
12782
  var CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
11603
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;
@@ -11606,6 +12786,70 @@ var RESULT_CLASS_ATTRIBUTE_PATTERN = /\bclass\s*=\s*(["'])([^"']*)\1/i;
11606
12786
  var RESULT_HREF_ATTRIBUTE_PATTERN = /\bhref\s*=\s*(["'])([^"']*)\1/i;
11607
12787
  var NEXT_RESULT_PATTERN = /<a\b[^>]*\bclass\s*=\s*(["'])[^"']*\bresult__a\b[^"']*\1/i;
11608
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
+ };
11609
12853
  var ResearchService = class {
11610
12854
  constructor(policy, logger) {
11611
12855
  this.policy = policy;
@@ -11613,6 +12857,7 @@ var ResearchService = class {
11613
12857
  }
11614
12858
  policy;
11615
12859
  logger;
12860
+ admission = new ResearchAdmission();
11616
12861
  async research(query, options = {}, signal) {
11617
12862
  if (typeof query !== "string") {
11618
12863
  throw new AppError("RESEARCH_INVALID", "A non-empty research query is required.");
@@ -11651,7 +12896,17 @@ var ResearchService = class {
11651
12896
  }, REQUEST_TIMEOUT_MS);
11652
12897
  const abort = () => controller.abort();
11653
12898
  signal?.addEventListener("abort", abort, { once: true });
12899
+ let release;
11654
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
+ }
11655
12910
  const url = await awaitWithAbort2(
11656
12911
  this.policy.assertNavigationAllowedAsync(`https://html.duckduckgo.com/html/?q=${encodedQuery}`),
11657
12912
  controller.signal
@@ -11722,9 +12977,13 @@ var ResearchService = class {
11722
12977
  } finally {
11723
12978
  clearTimeout(timeout);
11724
12979
  signal?.removeEventListener("abort", abort);
12980
+ release?.();
11725
12981
  }
11726
12982
  }
11727
12983
  };
12984
+ function cancelledResearchError() {
12985
+ return new AppError("CANCELLED", "The research request was cancelled.");
12986
+ }
11728
12987
  async function fetchWithRetry(url, signal) {
11729
12988
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
11730
12989
  if (signal.aborted) {
@@ -12254,10 +13513,11 @@ var ServerRuntime = class _ServerRuntime {
12254
13513
  mode: this.config.browser.mode,
12255
13514
  configured: managedBrowser || !browserDisabled && (usesExecutable ? Boolean(this.config.browser.executablePath) : Boolean(this.config.browser.wsEndpoint || this.config.browser.url)),
12256
13515
  connection: browserDisabled ? "disabled" : managedBrowser ? "managed" : usesExecutable ? "executable" : this.config.browser.wsEndpoint ? "websocket" : "devtools-http",
12257
- 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(),
12258
13517
  actionTimeoutMs: this.config.browser.actionTimeoutMs,
12259
13518
  connectTimeoutMs: this.config.browser.connectTimeoutMs,
12260
13519
  cdpTimeoutMs: this.config.browser.cdpTimeoutMs,
13520
+ idleTimeoutMs: this.config.browser.idleTimeoutMs,
12261
13521
  maxScreenshotBytes: this.config.browser.maxScreenshotBytes,
12262
13522
  maxHtmlChars: this.config.browser.maxHtmlChars
12263
13523
  },
@@ -12747,6 +14007,7 @@ async function serveHttp(runtime, shutdown) {
12747
14007
  response.on("error", (error) => runtime.logger.error("MCP HTTP response error", safeErrorDiagnostic(error)));
12748
14008
  request.on("error", (error) => runtime.logger.error("MCP HTTP request error", safeErrorDiagnostic(error)));
12749
14009
  if (!accepting) {
14010
+ closeIncompleteRequestAfterResponse(request, response);
12750
14011
  response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
12751
14012
  response.end(HTTP_SHUTTING_DOWN_BODY);
12752
14013
  return;
@@ -12759,11 +14020,13 @@ async function serveHttp(runtime, shutdown) {
12759
14020
  }
12760
14021
  setCorsHeaders(request, response);
12761
14022
  if (!requestPathMatches(request, config.http.path)) {
14023
+ closeIncompleteRequestAfterResponse(request, response);
12762
14024
  response.writeHead(404, { "content-type": "application/json" });
12763
14025
  response.end(HTTP_NOT_FOUND_BODY);
12764
14026
  return;
12765
14027
  }
12766
14028
  if (request.method === "OPTIONS") {
14029
+ closeIncompleteRequestAfterResponse(request, response);
12767
14030
  response.writeHead(204, {
12768
14031
  "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
12769
14032
  "access-control-allow-headers": request.headers["access-control-request-headers"] ?? "authorization, content-type, accept, mcp-protocol-version, mcp-session-id, last-event-id",
@@ -12774,6 +14037,7 @@ async function serveHttp(runtime, shutdown) {
12774
14037
  return;
12775
14038
  }
12776
14039
  if (!authorized(request, expectedAuthDigest)) {
14040
+ closeIncompleteRequestAfterResponse(request, response);
12777
14041
  response.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" });
12778
14042
  response.end(HTTP_UNAUTHORIZED_BODY);
12779
14043
  return;
@@ -12781,6 +14045,7 @@ async function serveHttp(runtime, shutdown) {
12781
14045
  let streamPool = isPotentialHttpStream(request) ? activeHttpStreams : activeHttpRequests;
12782
14046
  const poolLimit = streamPool === activeHttpStreams ? MAX_HTTP_STREAM_CONCURRENCY : MAX_HTTP_CONCURRENCY;
12783
14047
  if (streamPool.size >= poolLimit) {
14048
+ closeIncompleteRequestAfterResponse(request, response);
12784
14049
  response.writeHead(503, { "content-type": "application/json", "retry-after": "1" });
12785
14050
  response.end(HTTP_BUSY_BODY);
12786
14051
  return;