From cf366a4abd5442209ba45d658dcb589fdd82ab36 Mon Sep 17 00:00:00 2001 From: Ksenia Berezina Date: Sun, 12 Jul 2026 22:49:58 -0400 Subject: [PATCH 1/5] Add Chrome support to autowebcompat repro agent --- agents/autowebcompat-repro/Dockerfile | 19 ++-- .../autowebcompat_repro/agent.py | 95 +++++++++++++++---- .../autowebcompat_repro/browser.py | 89 +++++++++++++++++ .../chrome_devtools_mcp.py | 42 ++++++++ .../autowebcompat_repro/config.py | 31 +++++- .../autowebcompat_repro/result.py | 20 +++- 6 files changed, 264 insertions(+), 32 deletions(-) create mode 100644 agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py diff --git a/agents/autowebcompat-repro/Dockerfile b/agents/autowebcompat-repro/Dockerfile index 4740cc4ce6..ff5264b0fc 100644 --- a/agents/autowebcompat-repro/Dockerfile +++ b/agents/autowebcompat-repro/Dockerfile @@ -28,17 +28,22 @@ ENV PATH="/opt/venv/bin:$PATH" FROM base AS agent -# The Firefox DevTools MCP server is an npm package launched via `npx`, so the -# agent image needs Node.js + npm (the python base ships neither). It also -# needs the shared libraries Firefox requires to run headless; the Firefox -# binary itself is downloaded at agent startup (a fresh Nightly per run) via -# mozdownload/mozinstall, not baked in here. +# The Firefox and Chrome DevTools MCP servers are npm packages launched via +# `npx`, so the agent image needs Node.js + npm (the python base ships neither). +# It also needs the shared libraries the browsers require to run headless. The +# browser binaries themselves are downloaded at agent startup (a fresh build per +# run): Firefox via mozdownload/mozinstall and Chrome for Testing via the +# Chrome-for-Testing JSON API (see browser.py). The first `lib*` group below is +# Firefox's runtime deps; the rest are the extra deps headless Chrome needs. RUN apt-get update \ && apt-get install -y --no-install-recommends \ nodejs npm \ ca-certificates \ - libgtk-3-0 libdbus-glib-1-2 libx11-xcb1 libxtst6 libxt6 \ - libasound2 libpci3 \ + libgtk-3-0 libdbus-glib-1-2 libx11-xcb1 libxtst6 libxt6 libpci3 \ + libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 libatspi2.0-0t64 \ + libcups2t64 libdbus-1-3 libgbm1 libxcomposite1 libxdamage1 libxfixes3 \ + libxrandr2 libxkbcommon0 libasound2t64 libpango-1.0-0 libcairo2 \ + fonts-liberation \ && rm -rf /var/lib/apt/lists/* # hackbot.toml lives at the agent root (not inside the package), so copy it into diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py index 529bbfe722..ad4da76372 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py @@ -28,8 +28,9 @@ from hackbot_runtime.claude import Reporter from pydantic import BaseModel -from .browser import FirefoxBrowsers -from .config import BUGZILLA_READ_TOOLS, DEVTOOLS_TOOLS +from .browser import ChromeBrowsers, FirefoxBrowsers +from .chrome_devtools_mcp import build_chrome_devtools_server +from .config import BUGZILLA_READ_TOOLS, CHROME_DEVTOOLS_TOOLS, DEVTOOLS_TOOLS from .devtools_mcp import build_devtools_server from .result import ( RESULT_SERVER_NAME, @@ -82,6 +83,7 @@ class AutowebcompatReproResult(BaseModel): plan_result: TestPlanResult reproductions: list[tuple[str, BugReproductionResult | ReproductionResult]] chrome_mask_fixed: bool | None + chrome_reproduced: bool | None @dataclass @@ -317,9 +319,13 @@ def __init__( input_data: AutoWebcompatInput, bugzilla_mcp_server: McpServerConfig, screenshot_dir: Path, + chrome_path: Path | None = None, ): super().__init__(task_config, run_tracker) self.input_data = input_data + # Chrome cross-check is only added when a Chrome binary is supplied + # (the initial nightly attempt). Later channel attempts run Firefox-only. + self.cross_check_chrome = chrome_path is not None self.screenshot_path = make_empty_temp_file( screenshot_dir, "reproduction=", ".png" ) @@ -334,6 +340,16 @@ def __init__( ), DEVTOOLS_TOOLS, ) + # The initial reproduction also drives Chrome, so the agent can + # cross-check both browsers in one context: if the same steps behave + # identically in Firefox and Chrome it can iterate on whether the steps + # are wrong versus it genuinely not being a compat issue. + if chrome_path is not None: + self.add_mcp_server( + "chrome-devtools", + build_chrome_devtools_server(chrome_path=chrome_path, headless=True), + CHROME_DEVTOOLS_TOOLS, + ) if self.input_data.type == "bug_id" != None: self.add_mcp_server("bugzilla", bugzilla_mcp_server, BUGZILLA_READ_TOOLS) @@ -341,22 +357,47 @@ def subject(self) -> Any: return self.input_data.subject() def system_prompt(self) -> str: + if self.cross_check_chrome: + chrome_step = """ +3. Cross-check in Chrome: run the same steps in Chrome using the Chrome DevTools + MCP. A genuine web-compat issue reproduces in Firefox but not in in Chrome. + - If the behavior is identical in both browsers, your steps may be wrong or + this may not be a compat issue. Iterate: refine the steps and re-check both + browsers before concluding. Use the difference (or lack of it) between the + two browsers to decide whether you have really reproduced the reported + issue. + - Set `reproduced` for the Firefox outcome and `chrome_reproduced` for the + Chrome outcome (`true` if the issue also reproduces in Chrome, `false` if + the issue is not reproducible in Chrome).""" + else: + chrome_step = """ +3. No Chrome cross-check is available for this task: leave `chrome_reproduced` + null.""" + return ( super() .system_prompt() .format( task_details=f""" 1. Identify the affected URL and the described broken behavior. -2. Baseline: Navigate to the URL with the Firefox DevTools MCP and - try to reproduce the described broken behaviour. -3. If the issue reproduces AND the breakage is visual in nature (incorrect - layout or rendering, not broken interaction), capture a screenshot showing - it: call `screenshot_page` with `saveTo` set to `{self.screenshot_path}`. - This writes the image to that file instead of returning it — do not capture - or paste the image data yourself. Then set `screenshot_path` in your result - to exactly `{self.screenshot_path}`. For non-visual issues, take no - screenshot and leave `screenshot_path` null. -4. Submit your findings via `submit_result` (see "Reporting your result"). +2. Baseline: Navigate to the URL with the Firefox DevTools MCP + and try to reproduce the described broken behaviour. + - Reproduce against the actual reported site. If you cannot reach or + reproduce on that site — e.g. it is behind a login wall, blocked, + gated by a captcha, or down — do not substitute a reduced testcase, + minimal example, or other bug attachment as a stand-in reproduction. + Report `reproduced` as false with the appropriate `failure_reason`. A + testcase may inform your analysis, but reproducing on it does not count + as reproducing the reported site issue. +{chrome_step} +4. If the issue reproduces AND the breakage is visual in nature (incorrect + layout or rendering, not broken interaction), capture a screenshot in Firefox + showing it: call `screenshot_page` with `saveTo` set to + `{self.screenshot_path}`. This writes the image to that file instead of + returning it — do not capture or paste the image data yourself. Then set + `screenshot_path` in your result to exactly `{self.screenshot_path}`. For + non-visual issues, take no screenshot and leave `screenshot_path` null. +5. Submit your findings via `submit_result` (see "Reporting your result"). """ ) ) @@ -502,6 +543,7 @@ def __init__(self, publish_file: PublishFile, plan_result: TestPlanResult): ] = {} self.initial_repro: InitialReproduction | None = None self.chrome_mask_fixed: bool | None = None + self.chrome_reproduced: bool | None = None @property def reproduced(self) -> bool: @@ -551,12 +593,19 @@ def set_result( key = (channel, extra) if key in self.results: raise ValueError(f"Got duplicate results for {channel}, {extra}") - if isinstance(result, BugReproductionResult) and result.reproduced: - if self.initial_repro is not None: - raise ValueError("Got duplicate steps / summary") - self.initial_repro = InitialReproduction( - channel, result.steps, result.summary, result.screenshot_path - ) + if isinstance(result, BugReproductionResult): + if result.reproduced: + if self.initial_repro is not None: + raise ValueError("Got duplicate steps / summary") + self.initial_repro = InitialReproduction( + channel, result.steps, result.summary, result.screenshot_path + ) + # Only the Chrome-enabled (initial nightly) reproduction reports a + # Chrome verdict; other attempts leave it null. + if result.chrome_reproduced is not None: + if self.chrome_reproduced is not None: + raise ValueError("Got duplicate chrome cross-check results") + self.chrome_reproduced = result.chrome_reproduced elif isinstance(result, ChromeMaskResult): if self.chrome_mask_fixed is not None: raise ValueError("Got duplicate results for chrome mask") @@ -578,6 +627,7 @@ def into_result(self) -> AutowebcompatReproResult: if isinstance(value, ReproductionResult) ], chrome_mask_fixed=self.chrome_mask_fixed, + chrome_reproduced=self.chrome_reproduced, ) @@ -594,6 +644,7 @@ async def run_autowebcompat_repro( :class:`AgentError` if the agent ends in an error. """ firefox_browser = FirefoxBrowsers() + chrome_browser = ChromeBrowsers() test_plan_task = TestPlan(default_config, tracker, input_data, bugzilla_mcp_server) test_plan_result = await test_plan_task.run() @@ -614,6 +665,7 @@ async def next_repro_task( channel: FirefoxChannel, extra: str | None = None, config: TaskConfig = default_config, + cross_check_chrome: bool = False, ) -> None: browser = getattr(firefox_browser, channel.value) profile = setup_profile(browser) @@ -626,6 +678,9 @@ async def next_repro_task( input_data, bugzilla_mcp_server, screenshots_dir, + # Only cross-check Chrome when asked (the initial nightly + # attempt); later channel attempts stay Firefox-only. + chrome_path=chrome_browser.stable if cross_check_chrome else None, ) else: task = StepsReproduction( @@ -644,8 +699,8 @@ async def next_repro_task( screenshots_dir = Path(tempfile.mkdtemp(prefix="autowebcompat-screenshots-")) - # Always try in nightly first - await next_repro_task(FirefoxChannel.nightly) + # Always try in nightly first, cross-checking in Chrome. + await next_repro_task(FirefoxChannel.nightly, cross_check_chrome=True) if not repro_results.reproduced and test_plan_result.affects_platforms == [ "android" diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py index 189e3dd551..a06ff057af 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py @@ -1,14 +1,24 @@ import logging import platform +import stat import tempfile +import zipfile from pathlib import Path from typing import Literal import mozdownload import mozinstall +import requests logger = logging.getLogger("autowebcompat-repro") +CHROME_VERSIONS_URL = ( + "https://googlechromelabs.github.io/chrome-for-testing/" + "last-known-good-versions-with-downloads.json" +) +CHROME_DOWNLOAD_TIMEOUT = 120 +CHROME_CHUNK_SIZE = 1 << 20 + def install_firefox( channel: Literal["nightly"] | Literal["stable"] | Literal["esr"], @@ -72,3 +82,82 @@ def esr(self) -> Path: if self._esr is None: self._esr = install_firefox(channel="esr") return self._esr + + +def chrome_platform() -> str: + """Chrome for Testing platform string for the current host.""" + system = platform.system() + machine = platform.machine().lower() + if system == "Linux": + if machine not in {"x86_64", "amd64"}: + raise RuntimeError( + "Chrome for Testing has no linux build for " + f"{platform.machine()}; only x86_64/amd64 is supported. Run the " + "agent image as linux/amd64, e.g. DOCKER_DEFAULT_PLATFORM=linux/amd64." + ) + return "linux64" + if system == "Darwin": + return "mac-arm64" if machine in {"arm64", "aarch64"} else "mac-x64" + if system == "Windows": + return "win64" if machine in {"x86_64", "amd64"} else "win32" + raise RuntimeError(f"Unsupported platform for Chrome for Testing: {system}") + + +def resolve_chrome_download_url(channel: str, cft_platform: str) -> str: + """Look up the Chrome for Testing download URL for a channel + platform.""" + response = requests.get(CHROME_VERSIONS_URL, timeout=CHROME_DOWNLOAD_TIMEOUT) + response.raise_for_status() + data = response.json() + + entry = data["channels"][channel.capitalize()] + logger.info("Chrome for Testing %s: version %s", channel, entry["version"]) + + for download in entry["downloads"]["chrome"]: + if download["platform"] == cft_platform: + return download["url"] + + raise RuntimeError( + f"no Chrome for Testing '{cft_platform}' download in {channel} channel" + ) + + +def install_chrome(channel: Literal["stable"] = "stable") -> Path: + """Download Chrome for Testing and return the browser binary path.""" + cft_platform = chrome_platform() + install_dir = Path(tempfile.mkdtemp(prefix=f"chrome-{channel}-", dir=Path.home())) + + url = resolve_chrome_download_url(channel, cft_platform) + archive = install_dir / f"chrome-{cft_platform}.zip" + + logger.info("downloading Chrome for Testing from %s", url) + with requests.get(url, stream=True, timeout=CHROME_DOWNLOAD_TIMEOUT) as response: + response.raise_for_status() + with archive.open("wb") as out: + for chunk in response.iter_content(chunk_size=CHROME_CHUNK_SIZE): + if chunk: + out.write(chunk) + + with zipfile.ZipFile(archive) as zf: + zf.extractall(install_dir) + archive.unlink() + + binary = install_dir / f"chrome-{cft_platform}" / "chrome" + if not binary.exists(): + raise RuntimeError(f"Chrome binary not found at {binary} after unpacking") + + # zipfile does not preserve the executable bit; restore it. + binary.chmod(binary.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + + logger.info("installed Chrome at %s", binary) + return binary + + +class ChromeBrowsers: + def __init__(self) -> None: + self._stable: Path | None = None + + @property + def stable(self) -> Path: + if self._stable is None: + self._stable = install_chrome(channel="stable") + return self._stable diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py new file mode 100644 index 0000000000..3cbd24dfbf --- /dev/null +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from pathlib import Path + +from claude_agent_sdk.types import McpStdioServerConfig + +PACKAGE = "chrome-devtools-mcp@latest" + + +def build_chrome_devtools_server( + chrome_path: Path | None = None, + *, + headless: bool = True, + no_sandbox: bool = True, +) -> McpStdioServerConfig: + """Build the stdio config for the Chrome DevTools MCP server. + + Args: + chrome_path: Chrome binary to drive (the Chrome for Testing build from + ``browser.install_chrome``). When ``None`` the server lets its + bundled Puppeteer discover a Chrome installation itself. + headless: Run Chrome without a visible window (required in + container/CI environments). + no_sandbox: Pass ``--no-sandbox`` to Chrome. Required when running as an + unprivileged user inside a container, where Chrome's setuid sandbox + cannot initialize and the browser otherwise fails to launch. + """ + args = ["-y", PACKAGE] + if headless: + args.append("--headless") + if chrome_path is not None: + args += ["--executablePath", str(chrome_path)] + + # Opt out of the MCP server's own data collection: its usage statistics and + # the CrUX API calls that send performance-trace URLs to Google. This does + # not touch Chrome's own behavior, only what the MCP server itself reports. + args += ["--usageStatistics=false", "--performanceCrux=false"] + + if no_sandbox: + args += ["--chromeArg=--no-sandbox", "--chromeArg=--disable-setuid-sandbox"] + + return McpStdioServerConfig(command="npx", args=args) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/config.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/config.py index c331f40d28..84eb9b5395 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/config.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/config.py @@ -11,8 +11,7 @@ # the "firefox-devtools" server name. Web-compat reproduction subset: page # navigation, accessibility snapshots + UID-based interaction, console/network # inspection, screenshots, and scripted DOM probing (evaluate_script needs -# --enable-script). Privileged-context and extension tools are intentionally -# omitted for now. +# --enable-script). DEVTOOLS_TOOLS = [ "mcp__firefox-devtools__list_pages", "mcp__firefox-devtools__new_page", @@ -42,3 +41,31 @@ "mcp__firefox-devtools__get_firefox_info", "mcp__firefox-devtools__get_firefox_output", ] + +# Chrome DevTools MCP tools (chrome-devtools-mcp), exposed under the +# "chrome-devtools" server name. Web-compat reproduction subset mirroring the +# Firefox list: page navigation, accessibility snapshots + UID-based +# interaction, console/network inspection, screenshots, and scripted DOM +# probing (evaluate_script). +CHROME_DEVTOOLS_TOOLS = [ + "mcp__chrome-devtools__list_pages", + "mcp__chrome-devtools__new_page", + "mcp__chrome-devtools__navigate_page", + "mcp__chrome-devtools__select_page", + "mcp__chrome-devtools__close_page", + "mcp__chrome-devtools__take_snapshot", + "mcp__chrome-devtools__click", + "mcp__chrome-devtools__hover", + "mcp__chrome-devtools__fill", + "mcp__chrome-devtools__fill_form", + "mcp__chrome-devtools__drag", + "mcp__chrome-devtools__upload_file", + "mcp__chrome-devtools__list_console_messages", + "mcp__chrome-devtools__list_network_requests", + "mcp__chrome-devtools__get_network_request", + "mcp__chrome-devtools__take_screenshot", + "mcp__chrome-devtools__evaluate_script", + "mcp__chrome-devtools__handle_dialog", + "mcp__chrome-devtools__wait_for", + "mcp__chrome-devtools__resize_page", +] diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py index 4450428077..b9c1ada70f 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py @@ -112,13 +112,27 @@ def validate_screenshot_path(cls, path: Path | None) -> Path | None: class BugReproductionResult(ReproductionResult): - """Canonical result the agent produces for a web-compat investigation.""" + """Canonical result the agent produces for a web-compat investigation. + + Produced by the initial reproduction task, which drives both Firefox and + Chrome so it can cross-check the two browsers in a single context. + """ summary: str = Field( description="""A concise account of whether the issue represents a real webcompat issue i.e. it can be reproduced in Firefox.""" ) + chrome_reproduced: bool | None = Field( + description=( + "Result of running the cross-check step in Chrome: " + "true if the issue also reproduces in Chrome, false if the " + "issue does not reproduce, or null if this task had " + "no Chrome DevTools MCP. When the issue reproduces " + "in Firefox but not in Chrome, that indicates a Firefox-specific webcompat issue." + ), + ) + steps: str = Field( description=( "The ordered steps you took, as a single numbered list (1., 2., 3., " @@ -128,8 +142,8 @@ class BugReproductionResult(ReproductionResult): "provide (a file, image, account, or any other test data), state its " "exact origin — the URL you fetched it from, the command you ran, or " 'how you generated it — not just that you "used" or "saved" it. A ' - "reader must be able to obtain the same inputs. Omit the reproduction " - "screenshot step." + "reader must be able to obtain the same inputs. Omit the Chrome cross-check " + "reproduction and screenshot steps." ), ) From f7d9607b6506361e29e837d2524bc1fc133cc308 Mon Sep 17 00:00:00 2001 From: Ksenia Berezina Date: Mon, 13 Jul 2026 14:46:22 -0400 Subject: [PATCH 2/5] Move mcp servers install into npm, always run Chrome in BugReproduction, adjust prompts --- agents/autowebcompat-repro/Dockerfile | 22 +- .../autowebcompat_repro/agent.py | 76 +- .../chrome_devtools_mcp.py | 42 - .../autowebcompat_repro/devtools_mcp.py | 54 - .../autowebcompat_repro/mcp_servers.py | 109 + .../autowebcompat_repro/result.py | 13 +- .../autowebcompat_repro/setup_profile.py | 2 +- agents/autowebcompat-repro/package-lock.json | 1780 +++++++++++++++++ agents/autowebcompat-repro/package.json | 10 + 9 files changed, 1954 insertions(+), 154 deletions(-) delete mode 100644 agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py delete mode 100644 agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/devtools_mcp.py create mode 100644 agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py create mode 100644 agents/autowebcompat-repro/package-lock.json create mode 100644 agents/autowebcompat-repro/package.json diff --git a/agents/autowebcompat-repro/Dockerfile b/agents/autowebcompat-repro/Dockerfile index ff5264b0fc..8c301db90d 100644 --- a/agents/autowebcompat-repro/Dockerfile +++ b/agents/autowebcompat-repro/Dockerfile @@ -28,24 +28,32 @@ ENV PATH="/opt/venv/bin:$PATH" FROM base AS agent -# The Firefox and Chrome DevTools MCP servers are npm packages launched via -# `npx`, so the agent image needs Node.js + npm (the python base ships neither). -# It also needs the shared libraries the browsers require to run headless. The -# browser binaries themselves are downloaded at agent startup (a fresh build per -# run): Firefox via mozdownload/mozinstall and Chrome for Testing via the -# Chrome-for-Testing JSON API (see browser.py). The first `lib*` group below is -# Firefox's runtime deps; the rest are the extra deps headless Chrome needs. +# The agent needs Node.js + npm to run the DevTools MCP servers (npm packages; +# the python base ships neither) and the shared libraries the browsers require +# to run headless. The browser binaries themselves are downloaded at agent +# startup (a fresh build per run): Firefox via mozdownload/mozinstall and Chrome +# for Testing via the Chrome-for-Testing JSON API (see browser.py). RUN apt-get update \ && apt-get install -y --no-install-recommends \ + # Node.js, to run the Firefox and Chrome DevTools MCP servers. nodejs npm \ + # Shared by both browsers. ca-certificates \ + # Firefox runtime deps. libgtk-3-0 libdbus-glib-1-2 libx11-xcb1 libxtst6 libxt6 libpci3 \ + # Chrome runtime deps. libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 libatspi2.0-0t64 \ libcups2t64 libdbus-1-3 libgbm1 libxcomposite1 libxdamage1 libxfixes3 \ libxrandr2 libxkbcommon0 libasound2t64 libpango-1.0-0 libcairo2 \ fonts-liberation \ && rm -rf /var/lib/apt/lists/* +# Install the DevTools MCP servers from the pinned package.json/lockfile +COPY agents/autowebcompat-repro/package.json \ + agents/autowebcompat-repro/package-lock.json \ + /app/node/ +RUN cd /app/node && npm ci --omit=dev + # hackbot.toml lives at the agent root (not inside the package), so copy it into # the working dir; the runtime discovers it there (cwd) at startup. COPY agents/autowebcompat-repro/hackbot.toml /app/hackbot.toml diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py index ad4da76372..5642e58481 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py @@ -29,9 +29,8 @@ from pydantic import BaseModel from .browser import ChromeBrowsers, FirefoxBrowsers -from .chrome_devtools_mcp import build_chrome_devtools_server from .config import BUGZILLA_READ_TOOLS, CHROME_DEVTOOLS_TOOLS, DEVTOOLS_TOOLS -from .devtools_mcp import build_devtools_server +from .mcp_servers import build_chrome_devtools_server, build_firefox_devtools_server from .result import ( RESULT_SERVER_NAME, SUBMIT_RESULT_TOOL, @@ -319,19 +318,16 @@ def __init__( input_data: AutoWebcompatInput, bugzilla_mcp_server: McpServerConfig, screenshot_dir: Path, - chrome_path: Path | None = None, + chrome_path: Path, ): super().__init__(task_config, run_tracker) self.input_data = input_data - # Chrome cross-check is only added when a Chrome binary is supplied - # (the initial nightly attempt). Later channel attempts run Firefox-only. - self.cross_check_chrome = chrome_path is not None self.screenshot_path = make_empty_temp_file( screenshot_dir, "reproduction=", ".png" ) self.add_mcp_server( "firefox-devtools", - build_devtools_server( + build_firefox_devtools_server( firefox_path=firefox_path, headless=True, enable_script=True, @@ -340,40 +336,19 @@ def __init__( ), DEVTOOLS_TOOLS, ) - # The initial reproduction also drives Chrome, so the agent can - # cross-check both browsers in one context: if the same steps behave - # identically in Firefox and Chrome it can iterate on whether the steps - # are wrong versus it genuinely not being a compat issue. - if chrome_path is not None: - self.add_mcp_server( - "chrome-devtools", - build_chrome_devtools_server(chrome_path=chrome_path, headless=True), - CHROME_DEVTOOLS_TOOLS, - ) - if self.input_data.type == "bug_id" != None: + + self.add_mcp_server( + "chrome-devtools", + build_chrome_devtools_server(chrome_path=chrome_path, headless=True), + CHROME_DEVTOOLS_TOOLS, + ) + if self.input_data.type == "bug_id": self.add_mcp_server("bugzilla", bugzilla_mcp_server, BUGZILLA_READ_TOOLS) def subject(self) -> Any: return self.input_data.subject() def system_prompt(self) -> str: - if self.cross_check_chrome: - chrome_step = """ -3. Cross-check in Chrome: run the same steps in Chrome using the Chrome DevTools - MCP. A genuine web-compat issue reproduces in Firefox but not in in Chrome. - - If the behavior is identical in both browsers, your steps may be wrong or - this may not be a compat issue. Iterate: refine the steps and re-check both - browsers before concluding. Use the difference (or lack of it) between the - two browsers to decide whether you have really reproduced the reported - issue. - - Set `reproduced` for the Firefox outcome and `chrome_reproduced` for the - Chrome outcome (`true` if the issue also reproduces in Chrome, `false` if - the issue is not reproducible in Chrome).""" - else: - chrome_step = """ -3. No Chrome cross-check is available for this task: leave `chrome_reproduced` - null.""" - return ( super() .system_prompt() @@ -389,7 +364,13 @@ def system_prompt(self) -> str: Report `reproduced` as false with the appropriate `failure_reason`. A testcase may inform your analysis, but reproducing on it does not count as reproducing the reported site issue. -{chrome_step} +3. Cross-check in Chrome: run the same steps in Chrome using the Chrome DevTools + MCP and report `chrome_reproduced`. + - A genuine web-compat issue reproduces in Firefox but not in Chrome. If the + behavior is identical in both, your steps may be wrong or this may not be a + web-compat issue; refine the steps and re-check before concluding. + - If the reported broken behaviour reproduces in both browsers, it is not a + Firefox web-compat issue: set `failure_reason` to `non_compat`. 4. If the issue reproduces AND the breakage is visual in nature (incorrect layout or rendering, not broken interaction), capture a screenshot in Firefox showing it: call `screenshot_page` with `saveTo` set to @@ -432,7 +413,7 @@ def __init__( self.steps = steps self.add_mcp_server( "firefox_devtools", - build_devtools_server( + build_firefox_devtools_server( firefox_path=firefox_path, headless=True, enable_script=True, @@ -478,7 +459,7 @@ def __init__( self.steps = steps self.add_mcp_server( "firefox_devtools", - build_devtools_server( + build_firefox_devtools_server( firefox_path=firefox_path, headless=True, enable_script=True, @@ -665,11 +646,13 @@ async def next_repro_task( channel: FirefoxChannel, extra: str | None = None, config: TaskConfig = default_config, - cross_check_chrome: bool = False, ) -> None: browser = getattr(firefox_browser, channel.value) profile = setup_profile(browser) if repro_results.initial_repro is None: + # The initial reproduction drives Chrome as a baseline as well as + # Firefox, so it can tell a Firefox-specific issue apart from one + # that reproduces in both browsers. task: Task = BugReproduction( config, tracker, @@ -678,9 +661,7 @@ async def next_repro_task( input_data, bugzilla_mcp_server, screenshots_dir, - # Only cross-check Chrome when asked (the initial nightly - # attempt); later channel attempts stay Firefox-only. - chrome_path=chrome_browser.stable if cross_check_chrome else None, + chrome_browser.stable, ) else: task = StepsReproduction( @@ -699,8 +680,15 @@ async def next_repro_task( screenshots_dir = Path(tempfile.mkdtemp(prefix="autowebcompat-screenshots-")) - # Always try in nightly first, cross-checking in Chrome. - await next_repro_task(FirefoxChannel.nightly, cross_check_chrome=True) + # Always try in nightly first. + await next_repro_task(FirefoxChannel.nightly) + + # If the reported behavior reproduced in both Firefox and Chrome it is not a + # Firefox web-compat issue: stop early. + if repro_results.reproduced and repro_results.chrome_reproduced: + result = repro_results.into_result() + result.failure_reason = "non_compat" + return result if not repro_results.reproduced and test_plan_result.affects_platforms == [ "android" diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py deleted file mode 100644 index 3cbd24dfbf..0000000000 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/chrome_devtools_mcp.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from claude_agent_sdk.types import McpStdioServerConfig - -PACKAGE = "chrome-devtools-mcp@latest" - - -def build_chrome_devtools_server( - chrome_path: Path | None = None, - *, - headless: bool = True, - no_sandbox: bool = True, -) -> McpStdioServerConfig: - """Build the stdio config for the Chrome DevTools MCP server. - - Args: - chrome_path: Chrome binary to drive (the Chrome for Testing build from - ``browser.install_chrome``). When ``None`` the server lets its - bundled Puppeteer discover a Chrome installation itself. - headless: Run Chrome without a visible window (required in - container/CI environments). - no_sandbox: Pass ``--no-sandbox`` to Chrome. Required when running as an - unprivileged user inside a container, where Chrome's setuid sandbox - cannot initialize and the browser otherwise fails to launch. - """ - args = ["-y", PACKAGE] - if headless: - args.append("--headless") - if chrome_path is not None: - args += ["--executablePath", str(chrome_path)] - - # Opt out of the MCP server's own data collection: its usage statistics and - # the CrUX API calls that send performance-trace URLs to Google. This does - # not touch Chrome's own behavior, only what the MCP server itself reports. - args += ["--usageStatistics=false", "--performanceCrux=false"] - - if no_sandbox: - args += ["--chromeArg=--no-sandbox", "--chromeArg=--disable-setuid-sandbox"] - - return McpStdioServerConfig(command="npx", args=args) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/devtools_mcp.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/devtools_mcp.py deleted file mode 100644 index 119d995930..0000000000 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/devtools_mcp.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from claude_agent_sdk.types import McpStdioServerConfig - -PACKAGE = "@mozilla/firefox-devtools-mcp-moz" - - -def build_devtools_server( - firefox_path: Path | None = None, - *, - headless: bool = True, - enable_script: bool = True, - enable_privileged_context: bool = False, - profile_path: Path | None = None, -) -> McpStdioServerConfig: - """Build the stdio config for the Firefox DevTools MCP server. - - Args: - firefox_path: Firefox binary to drive. When ``None`` the server - auto-detects an installed Firefox. - headless: Run Firefox without a visible window (required in - container/CI environments). - enable_script: Expose the ``evaluate_script`` tool, which runs - arbitrary JS in the page context. - enable_privileged_context: Expose the privileged-context tools - (``list_extensions``, ``evaluate_privileged_script``, prefs, etc.) - and set ``MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`` on the Firefox process. - Required for the Chrome Mask flow: the agent needs ``list_extensions`` - to resolve the extension's ``moz-extension:///`` base URL, and - navigating to that privileged origin is itself blocked without this. - profile_path: A pre-built Firefox profile to use as a template (e.g. - one with the Chrome Mask extension installed). geckodriver copies - it into a fresh per-session profile, so the template is not - mutated. When ``None`` the server uses a clean throwaway profile. - """ - args = [PACKAGE] - if headless: - args.append("--headless") - if enable_script: - args.append("--enable-script") - if enable_privileged_context: - args.append("--enable-privileged-context") - if firefox_path is not None: - args += ["--firefox-path", str(firefox_path)] - if profile_path is not None: - args += ["--profile-path", str(profile_path)] - - if enable_privileged_context: - return McpStdioServerConfig( - command="npx", args=args, env={"MOZ_REMOTE_ALLOW_SYSTEM_ACCESS": "1"} - ) - return McpStdioServerConfig(command="npx", args=args) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py new file mode 100644 index 0000000000..11541ede32 --- /dev/null +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py @@ -0,0 +1,109 @@ +"""Stdio configs for the DevTools MCP servers the agent drives. + +The MCP servers are npm packages pinned in ``package.json`` and installed into +the image with ``npm ci`` (see the Dockerfile). +""" + +from __future__ import annotations + +from pathlib import Path + +from claude_agent_sdk.types import McpStdioServerConfig + +NODE_PROJECT_DIR = Path("/app/node") +FIREFOX_DEVTOOLS_BIN = "firefox-devtools-mcp-moz" +CHROME_DEVTOOLS_BIN = "chrome-devtools-mcp" + + +def resolve_bin(bin_name: str) -> str: + """Resolve an installed MCP server binary to an absolute path.""" + binary = NODE_PROJECT_DIR / "node_modules" / ".bin" / bin_name + if not binary.exists(): + raise RuntimeError( + f"MCP server binary not found at {binary}; the image should install " + f"it with `npm ci` (see the Dockerfile)." + ) + return str(binary) + + +def build_firefox_devtools_server( + firefox_path: Path | None = None, + *, + headless: bool = True, + enable_script: bool = True, + enable_privileged_context: bool = False, + profile_path: Path | None = None, +) -> McpStdioServerConfig: + """Build the stdio config for the Firefox DevTools MCP server. + + Args: + firefox_path: Firefox binary to drive. When ``None`` the server + auto-detects an installed Firefox. + headless: Run Firefox without a visible window (required in + container/CI environments). + enable_script: Expose the ``evaluate_script`` tool, which runs + arbitrary JS in the page context. + enable_privileged_context: Expose the privileged-context tools + (``list_extensions``, ``evaluate_privileged_script``, prefs, etc.) + and set ``MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1`` on the Firefox process. + Required for the Chrome Mask flow: the agent needs ``list_extensions`` + to resolve the extension's ``moz-extension:///`` base URL, and + navigating to that privileged origin is itself blocked without this. + profile_path: A pre-built Firefox profile to use as a template (e.g. + one with the Chrome Mask extension installed). geckodriver copies + it into a fresh per-session profile, so the template is not + mutated. When ``None`` the server uses a clean throwaway profile. + """ + args = [] + if headless: + args.append("--headless") + if enable_script: + args.append("--enable-script") + if enable_privileged_context: + args.append("--enable-privileged-context") + if firefox_path is not None: + args += ["--firefox-path", str(firefox_path)] + if profile_path is not None: + args += ["--profile-path", str(profile_path)] + + command = resolve_bin(FIREFOX_DEVTOOLS_BIN) + if enable_privileged_context: + return McpStdioServerConfig( + command=command, args=args, env={"MOZ_REMOTE_ALLOW_SYSTEM_ACCESS": "1"} + ) + return McpStdioServerConfig(command=command, args=args) + + +def build_chrome_devtools_server( + chrome_path: Path | None = None, + *, + headless: bool = True, + no_sandbox: bool = True, +) -> McpStdioServerConfig: + """Build the stdio config for the Chrome DevTools MCP server. + + Args: + chrome_path: Chrome binary to drive (the Chrome for Testing build from + ``browser.install_chrome``). When ``None`` the server lets its + bundled Puppeteer discover a Chrome installation itself. + headless: Run Chrome without a visible window (required in + container/CI environments). + no_sandbox: Pass ``--no-sandbox`` to Chrome. Required when running as an + unprivileged user inside a container, where Chrome's setuid sandbox + cannot initialize and the browser otherwise fails to launch. + """ + args = [] + if headless: + args.append("--headless") + if chrome_path is not None: + args += ["--executablePath", str(chrome_path)] + + # Opt out of the MCP server's own data collection: its usage statistics and + # the CrUX API calls that send performance-trace URLs to Google. This does + # not touch Chrome's own behavior, only what the MCP server itself reports. + args += ["--usageStatistics=false", "--performanceCrux=false"] + + if no_sandbox: + args += ["--chromeArg=--no-sandbox", "--chromeArg=--disable-setuid-sandbox"] + + return McpStdioServerConfig(command=resolve_bin(CHROME_DEVTOOLS_BIN), args=args) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py index b9c1ada70f..239564a385 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py @@ -74,10 +74,12 @@ class ReproductionResult(BaseModel): | Literal["other"] | None ) = Field( - description="""If an issue was reproduced then `null`. When an issue could not be reproduced, one of - following categories describing the reason for the failure: + description="""If an issue was reproduced as a Firefox web-compat issue then `null`. + Otherwise, one of the following categories describing the reason for the failure: * not_reproducable - When it was possible to run all the steps to reproduce, but no issue was found - * non_compat - When the report doesn't refer to site breakage for example for issues with the Firefox UI or product features such as reader mode + * non_compat - When the issue is not a Firefox web-compat issue. This covers reports that don't refer + to site breakage (e.g. issues with the Firefox UI or product features such as reader mode) and reports + whose behavior reproduces identically in both Firefox and Chrome. * unsupported_platform - When the report is specific to a platform that isn't available e.g. iOS * blocked_captcha - When access to the site was blocked because the page requires solving a captcha * blocked_geo - When access to the site was blocked based on location ("geoblocking") @@ -127,9 +129,8 @@ class BugReproductionResult(ReproductionResult): description=( "Result of running the cross-check step in Chrome: " "true if the issue also reproduces in Chrome, false if the " - "issue does not reproduce, or null if this task had " - "no Chrome DevTools MCP. When the issue reproduces " - "in Firefox but not in Chrome, that indicates a Firefox-specific webcompat issue." + "issue does not reproduce in Chrome, or null if the Chrome " + "cross-check wasn't able to confirm reproduction." ), ) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/setup_profile.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/setup_profile.py index 25467f7c7f..d7ce2099d1 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/setup_profile.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/setup_profile.py @@ -193,7 +193,7 @@ def setup_profile(firefox_path: Path, extensions: Sequence[str] = ()) -> Path: ``extensions`` is a list of AMO addon slugs (e.g. ``["chrome-mask"]``); each is downloaded and installed. With no extensions an empty profile parent is returned and no warm launch happens. The returned path is meant to be passed as the - devtools MCP's ``--profile-path`` (``build_devtools_server(profile_path=...)``). + devtools MCP's ``--profile-path`` (``build_firefox_devtools_server(profile_path=...)``). Raises ``RuntimeError`` if an extension does not end up registered and enabled in the profile. diff --git a/agents/autowebcompat-repro/package-lock.json b/agents/autowebcompat-repro/package-lock.json new file mode 100644 index 0000000000..863531ace5 --- /dev/null +++ b/agents/autowebcompat-repro/package-lock.json @@ -0,0 +1,1780 @@ +{ + "name": "hackbot-agent-autowebcompat-repro-mcp", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hackbot-agent-autowebcompat-repro-mcp", + "version": "0.0.0", + "dependencies": { + "@mozilla/firefox-devtools-mcp-moz": "0.9.12", + "chrome-devtools-mcp": "1.5.0" + } + }, + "node_modules/@bazel/runfiles": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", + "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==", + "license": "Apache-2.0" + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@mozilla/firefox-devtools-mcp-moz": { + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/@mozilla/firefox-devtools-mcp-moz/-/firefox-devtools-mcp-moz-0.9.12.tgz", + "integrity": "sha512-leHvHKfZsUvmJoDSDwoDmxctNNNXIk2rjMvKmqUoc00BxqrB954kmJGn/JrJIwFRsr49NKKy8xQRrHqp2eUndA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "1.29.0", + "geckodriver": "6.0.2", + "selenium-webdriver": "4.36.0", + "ws": "8.21.0", + "yargs": "17.7.2" + }, + "bin": { + "firefox-devtools-mcp-moz": "dist.moz/index.js" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@wdio/logger": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.29.1.tgz", + "integrity": "sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.28.tgz", + "integrity": "sha512-bqf5lkvRZnEn0n2SKgEh5Fz7nmieCS9RJ/juCjc7c5SP/mXAR3iZxp5ZhLyq1gee4z/s3FTDsVYmAhpK4Rb4kA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-devtools-mcp": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.5.0.tgz", + "integrity": "sha512-Yfpeg6cKnWaFrq/CpTY19bVO0kr84CpHNgTSQXTQshovKcRIf1efh1vAI+IOuCXrQvrul2hE0XoKPn94LRxC1A==", + "license": "Apache-2.0", + "bin": { + "chrome-devtools": "build/src/bin/chrome-devtools.js", + "chrome-devtools-mcp": "build/src/bin/chrome-devtools-mcp.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + }, + "peerDependencies": { + "@toon-format/toon": "^2.2.0" + }, + "peerDependenciesMeta": { + "@toon-format/toon": { + "optional": true + } + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/geckodriver": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.0.2.tgz", + "integrity": "sha512-5C4cejCvcz4yFiBHP0FEWKwSZKhr3WMkshNBQ7cSb9PqfrdSNVAbe2RTNxsdBkk3Fmqcu6PNnffiJa0T+JZGFQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.54", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "modern-tar": "^0.3.4" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/modern-tar": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.3.5.tgz", + "integrity": "sha512-TIALaZ8AjtEHFOZj1wRreDfaobCybvzPkvevpup/XtKOha3TmJWSwrh0ghc/QwAdAtt6oqIN6z6eIlo+HbDnzg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/selenium-webdriver": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.36.0.tgz", + "integrity": "sha512-rZGqjXiqNVL6QNqKNEk5DPaIMPbvApcmAS9QsXyt5wT3sfTSHGCh4AX/YKeDTOwei1BOZDlPOKBd82WCosUt9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/SeleniumHQ" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/selenium" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@bazel/runfiles": "^6.3.1", + "jszip": "^3.10.1", + "tmp": "^0.2.5", + "ws": "^8.18.3" + }, + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/agents/autowebcompat-repro/package.json b/agents/autowebcompat-repro/package.json new file mode 100644 index 0000000000..e019291d0e --- /dev/null +++ b/agents/autowebcompat-repro/package.json @@ -0,0 +1,10 @@ +{ + "name": "hackbot-agent-autowebcompat-repro-mcp", + "version": "1.0.0", + "private": true, + "description": "MCP servers the autowebcompat-repro agent drives.", + "dependencies": { + "@mozilla/firefox-devtools-mcp-moz": "0.9.12", + "chrome-devtools-mcp": "1.5.0" + } +} From 2e5e98837bec2b93a1d78be642a70d7a48b06785 Mon Sep 17 00:00:00 2001 From: Ksenia Berezina Date: Mon, 13 Jul 2026 20:41:18 -0400 Subject: [PATCH 3/5] Code review changes for autowebcompat chrome install --- .../autowebcompat_repro/browser.py | 83 +++++++++++++++---- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py index a06ff057af..c8d5a28b0b 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/browser.py @@ -1,6 +1,8 @@ import logging +import os import platform import stat +import sys import tempfile import zipfile from pathlib import Path @@ -12,13 +14,6 @@ logger = logging.getLogger("autowebcompat-repro") -CHROME_VERSIONS_URL = ( - "https://googlechromelabs.github.io/chrome-for-testing/" - "last-known-good-versions-with-downloads.json" -) -CHROME_DOWNLOAD_TIMEOUT = 120 -CHROME_CHUNK_SIZE = 1 << 20 - def install_firefox( channel: Literal["nightly"] | Literal["stable"] | Literal["esr"], @@ -105,7 +100,11 @@ def chrome_platform() -> str: def resolve_chrome_download_url(channel: str, cft_platform: str) -> str: """Look up the Chrome for Testing download URL for a channel + platform.""" - response = requests.get(CHROME_VERSIONS_URL, timeout=CHROME_DOWNLOAD_TIMEOUT) + versions_url = ( + "https://googlechromelabs.github.io/chrome-for-testing/" + "last-known-good-versions-with-downloads.json" + ) + response = requests.get(versions_url, timeout=120) response.raise_for_status() data = response.json() @@ -121,6 +120,62 @@ def resolve_chrome_download_url(channel: str, cft_platform: str) -> str: ) +def chrome_binary_path(install_dir: Path, cft_platform: str) -> Path: + """Path to the Chrome for Testing binary within the unpacked archive. + + Chrome for Testing uses a different executable name/layout per platform: + on macOS it is inside an `.app` bundle, on Windows it is `chrome.exe`, + and on Linux it is a `chrome`. + """ + package = install_dir / f"chrome-{cft_platform}" + if cft_platform.startswith("mac"): + return ( + package + / "Google Chrome for Testing.app" + / "Contents" + / "MacOS" + / "Google Chrome for Testing" + ) + if cft_platform.startswith("win"): + return package / "chrome.exe" + return package / "chrome" + + +def unzip(archive: Path, dest: Path) -> None: + """Extract a zip, preserving unix permission bits and recreating symlinks. + + This keeps the Chrome binary executable and, on macOS, keeps the ``.app`` + bundle's internal symlinks intact. + + Adapted from wpt's tools/wpt/utils.py::unzip. + """ + with zipfile.ZipFile(archive) as zip_data: + for info in zip_data.infolist(): + # external_attr's two high bytes carry the unix st_mode, but only + # when the archive was created on a unix system (create_system == 3). + # A DOS/Windows-created archive, or extraction on Windows, carries no + # useful permission info, so fall back to a plain extract there. + if info.create_system == 0 or sys.platform == "win32": + zip_data.extract(info, path=dest) + continue + + st_mode = info.external_attr >> 16 + dst_path = os.path.join(dest, info.filename) + if stat.S_ISLNK(st_mode): + # Symlinks are stored as files whose contents are the target; + # recreate the link rather than extracting it as a file. + link_target = zip_data.read(info) + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + if os.path.islink(dst_path): + os.unlink(dst_path) + os.symlink(link_target, dst_path) + else: + zip_data.extract(info, path=dest) + # Preserve the permission bits (rwxrwxrwx) only, dropping the + # sticky/setuid/setgid bits. + os.chmod(dst_path, st_mode & 0o777) + + def install_chrome(channel: Literal["stable"] = "stable") -> Path: """Download Chrome for Testing and return the browser binary path.""" cft_platform = chrome_platform() @@ -130,24 +185,20 @@ def install_chrome(channel: Literal["stable"] = "stable") -> Path: archive = install_dir / f"chrome-{cft_platform}.zip" logger.info("downloading Chrome for Testing from %s", url) - with requests.get(url, stream=True, timeout=CHROME_DOWNLOAD_TIMEOUT) as response: + with requests.get(url, stream=True, timeout=120) as response: response.raise_for_status() with archive.open("wb") as out: - for chunk in response.iter_content(chunk_size=CHROME_CHUNK_SIZE): + for chunk in response.iter_content(chunk_size=1 << 20): if chunk: out.write(chunk) - with zipfile.ZipFile(archive) as zf: - zf.extractall(install_dir) + unzip(archive, install_dir) archive.unlink() - binary = install_dir / f"chrome-{cft_platform}" / "chrome" + binary = chrome_binary_path(install_dir, cft_platform) if not binary.exists(): raise RuntimeError(f"Chrome binary not found at {binary} after unpacking") - # zipfile does not preserve the executable bit; restore it. - binary.chmod(binary.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) - logger.info("installed Chrome at %s", binary) return binary From 1194e7a741e7abd613730a94558907753f10d836 Mon Sep 17 00:00:00 2001 From: Ksenia Berezina Date: Mon, 13 Jul 2026 23:25:51 -0400 Subject: [PATCH 4/5] Make chrome_reproduced to be part of InitialReproduction dataclass --- .../autowebcompat_repro/agent.py | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py index 5642e58481..c0d790945a 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py @@ -513,6 +513,7 @@ class InitialReproduction: steps: str summary: str screenshot_path: Path | None + chrome_reproduced: bool | None class ReproductionResults: @@ -524,12 +525,27 @@ def __init__(self, publish_file: PublishFile, plan_result: TestPlanResult): ] = {} self.initial_repro: InitialReproduction | None = None self.chrome_mask_fixed: bool | None = None - self.chrome_reproduced: bool | None = None @property def reproduced(self) -> bool: return self.initial_repro is not None + @property + def chrome_reproduced(self) -> bool | None: + # Prefer the Chrome verdict from the channel that reproduced in Firefox, + # since that is the meaningful cross-check. If nothing reproduced in + # Firefox, still surface the first Chrome verdict we got (since the cross-check + # ran on every BugReproduction attempt). + if self.initial_repro is not None: + return self.initial_repro.chrome_reproduced + for result in self.results.values(): + if ( + isinstance(result, BugReproductionResult) + and result.chrome_reproduced is not None + ): + return result.chrome_reproduced + return None + @property def summary(self) -> str: return self.initial_repro.summary if self.initial_repro is not None else "" @@ -574,19 +590,16 @@ def set_result( key = (channel, extra) if key in self.results: raise ValueError(f"Got duplicate results for {channel}, {extra}") - if isinstance(result, BugReproductionResult): - if result.reproduced: - if self.initial_repro is not None: - raise ValueError("Got duplicate steps / summary") - self.initial_repro = InitialReproduction( - channel, result.steps, result.summary, result.screenshot_path - ) - # Only the Chrome-enabled (initial nightly) reproduction reports a - # Chrome verdict; other attempts leave it null. - if result.chrome_reproduced is not None: - if self.chrome_reproduced is not None: - raise ValueError("Got duplicate chrome cross-check results") - self.chrome_reproduced = result.chrome_reproduced + if isinstance(result, BugReproductionResult) and result.reproduced: + if self.initial_repro is not None: + raise ValueError("Got duplicate steps / summary") + self.initial_repro = InitialReproduction( + channel, + result.steps, + result.summary, + result.screenshot_path, + result.chrome_reproduced, + ) elif isinstance(result, ChromeMaskResult): if self.chrome_mask_fixed is not None: raise ValueError("Got duplicate results for chrome mask") @@ -650,9 +663,6 @@ async def next_repro_task( browser = getattr(firefox_browser, channel.value) profile = setup_profile(browser) if repro_results.initial_repro is None: - # The initial reproduction drives Chrome as a baseline as well as - # Firefox, so it can tell a Firefox-specific issue apart from one - # that reproduces in both browsers. task: Task = BugReproduction( config, tracker, @@ -680,7 +690,7 @@ async def next_repro_task( screenshots_dir = Path(tempfile.mkdtemp(prefix="autowebcompat-screenshots-")) - # Always try in nightly first. + # Always try in nightly first await next_repro_task(FirefoxChannel.nightly) # If the reported behavior reproduced in both Firefox and Chrome it is not a From e6039ecef533192e52b687a2c7e40d60fdd51b7f Mon Sep 17 00:00:00 2001 From: Ksenia Berezina Date: Tue, 14 Jul 2026 11:07:11 -0400 Subject: [PATCH 5/5] Code review changes --- .../autowebcompat_repro/agent.py | 25 +++++++++++-------- .../autowebcompat_repro/mcp_servers.py | 10 +++----- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py index c0d790945a..8047eb020d 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/agent.py @@ -532,19 +532,16 @@ def reproduced(self) -> bool: @property def chrome_reproduced(self) -> bool | None: - # Prefer the Chrome verdict from the channel that reproduced in Firefox, - # since that is the meaningful cross-check. If nothing reproduced in - # Firefox, still surface the first Chrome verdict we got (since the cross-check - # ran on every BugReproduction attempt). if self.initial_repro is not None: return self.initial_repro.chrome_reproduced - for result in self.results.values(): - if ( - isinstance(result, BugReproductionResult) - and result.chrome_reproduced is not None - ): - return result.chrome_reproduced - return None + + chrome_reproductions = [ + result.chrome_reproduced + for result in self.results.values() + if isinstance(result, BugReproductionResult) + and result.chrome_reproduced is not None + ] + return any(chrome_reproductions) if chrome_reproductions else None @property def summary(self) -> str: @@ -697,6 +694,12 @@ async def next_repro_task( # Firefox web-compat issue: stop early. if repro_results.reproduced and repro_results.chrome_reproduced: result = repro_results.into_result() + if result.failure_reason != "non_compat": + logger.warning( + "Issue reproduced in both Firefox and Chrome but failure_reason " + "was %r, should have been non_compat", + result.failure_reason, + ) result.failure_reason = "non_compat" return result diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py index 11541ede32..9cbdd380dc 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/mcp_servers.py @@ -10,14 +10,10 @@ from claude_agent_sdk.types import McpStdioServerConfig -NODE_PROJECT_DIR = Path("/app/node") -FIREFOX_DEVTOOLS_BIN = "firefox-devtools-mcp-moz" -CHROME_DEVTOOLS_BIN = "chrome-devtools-mcp" - def resolve_bin(bin_name: str) -> str: """Resolve an installed MCP server binary to an absolute path.""" - binary = NODE_PROJECT_DIR / "node_modules" / ".bin" / bin_name + binary = Path("/app/node") / "node_modules" / ".bin" / bin_name if not binary.exists(): raise RuntimeError( f"MCP server binary not found at {binary}; the image should install " @@ -66,7 +62,7 @@ def build_firefox_devtools_server( if profile_path is not None: args += ["--profile-path", str(profile_path)] - command = resolve_bin(FIREFOX_DEVTOOLS_BIN) + command = resolve_bin("firefox-devtools-mcp-moz") if enable_privileged_context: return McpStdioServerConfig( command=command, args=args, env={"MOZ_REMOTE_ALLOW_SYSTEM_ACCESS": "1"} @@ -106,4 +102,4 @@ def build_chrome_devtools_server( if no_sandbox: args += ["--chromeArg=--no-sandbox", "--chromeArg=--disable-setuid-sandbox"] - return McpStdioServerConfig(command=resolve_bin(CHROME_DEVTOOLS_BIN), args=args) + return McpStdioServerConfig(command=resolve_bin("chrome-devtools-mcp"), args=args)