dryhack-mcp 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/PKG-INFO +20 -10
  2. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/README.md +18 -9
  3. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/__init__.py +1 -1
  4. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/config.py +16 -0
  5. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/browser_tool.py +64 -11
  6. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/PKG-INFO +20 -10
  7. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/requires.txt +1 -0
  8. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/pyproject.toml +4 -1
  9. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/tests/test_browser.py +47 -2
  10. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/LICENSE +0 -0
  11. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/__main__.py +0 -0
  12. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/server.py +0 -0
  13. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/__init__.py +0 -0
  14. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/authorize.py +0 -0
  15. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/curl_tool.py +0 -0
  16. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/exec.py +0 -0
  17. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/python_tool.py +0 -0
  18. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/scope.py +0 -0
  19. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp/tools/shell_tool.py +0 -0
  20. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/SOURCES.txt +0 -0
  21. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/dependency_links.txt +0 -0
  22. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/entry_points.txt +0 -0
  23. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/dryhack_mcp.egg-info/top_level.txt +0 -0
  24. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/setup.cfg +0 -0
  25. {dryhack_mcp-2.2.0 → dryhack_mcp-2.3.0}/tests/test_authorize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dryhack-mcp
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing.
5
5
  Author: Afrizal F.A
6
6
  License: MIT License
@@ -36,6 +36,7 @@ Requires-Python: >=3.9
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
38
  Requires-Dist: mcp<2,>=1.2.0
39
+ Requires-Dist: playwright>=1.40
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: pytest>=7.0; extra == "dev"
41
42
  Requires-Dist: ruff>=0.1; extra == "dev"
@@ -88,16 +89,21 @@ python3 -m pip install -e ".[dev]"
88
89
 
89
90
  ### Browser tool (Playwright)
90
91
 
91
- The `browser` tool needs the optional `browser` extra plus the Playwright
92
- browser binaries:
92
+ Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
93
+ `uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
94
+ (Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
95
+ `browser` tool launches an engine it **auto-runs `playwright install <browser>`**
96
+ if the binary is missing.
97
+
98
+ To pre-install the binaries (optional, avoids the one-time first-use download):
93
99
 
94
100
  ```bash
95
- python3 -m pip install 'dryhack-mcp[browser]'
96
- playwright install # or: playwright install chromium
101
+ playwright install # all engines
102
+ playwright install chromium # just chromium
97
103
  ```
98
104
 
99
- If Playwright is not installed, the `browser` tool returns an install hint
100
- instead of failing — the rest of the server keeps working.
105
+ Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
106
+ to `0` to disable and manage browsers yourself.
101
107
 
102
108
  ## Run
103
109
 
@@ -181,6 +187,8 @@ Start the server separately with `dryhack-mcp --transport http`.
181
187
  | `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
182
188
  | `DRYHACK_WORKDIR` | cwd | Working directory for commands |
183
189
  | `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
190
+ | `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
191
+ | `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
184
192
  | `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
185
193
  | `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
186
194
 
@@ -236,9 +244,11 @@ browser(action="goto", url="https://target.example/",
236
244
  init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
237
245
  ```
238
246
 
239
- Playwright runs in a dedicated worker thread (the MCP server owns the asyncio
240
- loop, and Playwright's sync API cannot run inside a live loop). The same
241
- authorized-use scope rules as the other tools apply.
247
+ Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
248
+ server owns the asyncio loop, and Playwright's sync API cannot run inside a live
249
+ loop). The browser binary auto-installs on first use if missing
250
+ (`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
251
+ rules as the other tools apply.
242
252
 
243
253
  ## authorize (per-call scope)
244
254
 
@@ -43,16 +43,21 @@ python3 -m pip install -e ".[dev]"
43
43
 
44
44
  ### Browser tool (Playwright)
45
45
 
46
- The `browser` tool needs the optional `browser` extra plus the Playwright
47
- browser binaries:
46
+ Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
47
+ `uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
48
+ (Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
49
+ `browser` tool launches an engine it **auto-runs `playwright install <browser>`**
50
+ if the binary is missing.
51
+
52
+ To pre-install the binaries (optional, avoids the one-time first-use download):
48
53
 
49
54
  ```bash
50
- python3 -m pip install 'dryhack-mcp[browser]'
51
- playwright install # or: playwright install chromium
55
+ playwright install # all engines
56
+ playwright install chromium # just chromium
52
57
  ```
53
58
 
54
- If Playwright is not installed, the `browser` tool returns an install hint
55
- instead of failing — the rest of the server keeps working.
59
+ Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
60
+ to `0` to disable and manage browsers yourself.
56
61
 
57
62
  ## Run
58
63
 
@@ -136,6 +141,8 @@ Start the server separately with `dryhack-mcp --transport http`.
136
141
  | `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
137
142
  | `DRYHACK_WORKDIR` | cwd | Working directory for commands |
138
143
  | `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
144
+ | `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
145
+ | `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
139
146
  | `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
140
147
  | `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
141
148
 
@@ -191,9 +198,11 @@ browser(action="goto", url="https://target.example/",
191
198
  init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
192
199
  ```
193
200
 
194
- Playwright runs in a dedicated worker thread (the MCP server owns the asyncio
195
- loop, and Playwright's sync API cannot run inside a live loop). The same
196
- authorized-use scope rules as the other tools apply.
201
+ Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
202
+ server owns the asyncio loop, and Playwright's sync API cannot run inside a live
203
+ loop). The browser binary auto-installs on first use if missing
204
+ (`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
205
+ rules as the other tools apply.
197
206
 
198
207
  ## authorize (per-call scope)
199
208
 
@@ -1,3 +1,3 @@
1
1
  """DryHack-MCP: offensive-security MCP server for authorized penetration testing."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.3.0"
@@ -23,6 +23,22 @@ WORKDIR = _get("DRYHACK_WORKDIR", os.getcwd())
23
23
  OUTPUT_BYTE_LIMIT = int(_get("DRYHACK_OUTPUT_LIMIT", "65536") or "65536")
24
24
 
25
25
 
26
+ # ---- Browser tool (Playwright) -----------------------------------------------
27
+ # Auto-download the requested browser binary on first use if it is missing
28
+ # (the binaries are not shipped via pip). Disable by setting to 0/false/no.
29
+ BROWSER_AUTO_INSTALL = _get("DRYHACK_BROWSER_AUTO_INSTALL", "1").lower() not in (
30
+ "0",
31
+ "false",
32
+ "no",
33
+ "off",
34
+ )
35
+
36
+ # Timeout (seconds) for the on-demand `playwright install <browser>` download.
37
+ BROWSER_INSTALL_TIMEOUT = int(
38
+ _get("DRYHACK_BROWSER_INSTALL_TIMEOUT", "900") or "900"
39
+ )
40
+
41
+
26
42
  # ---- Transport ---------------------------------------------------------------
27
43
  # Default HTTP bind address when running in http mode.
28
44
  HTTP_HOST = _get("DRYHACK_HTTP_HOST", "127.0.0.1")
@@ -6,10 +6,13 @@ and exploitation: navigation, DOM interaction, arbitrary JavaScript injection
6
6
  file upload, cookie/header/storage manipulation, screenshots, PDF capture and
7
7
  multi-step action chains — all in a single MCP call.
8
8
 
9
- Playwright is an *optional* dependency (installed via the ``browser`` extra:
10
- ``pip install 'dryhack-mcp[browser]'`` followed by ``playwright install``). It is
11
- imported lazily so the core server keeps depending only on ``mcp``; if it is not
12
- installed the tool returns an actionable install hint instead of crashing.
9
+ Playwright is a bundled runtime dependency, so a plain ``pip install
10
+ dryhack-mcp`` / ``uvx dryhack-mcp`` already ships it. The browser *binaries*
11
+ (Chromium/Firefox/WebKit) are not distributed via pip, so on first use the tool
12
+ auto-runs ``playwright install <browser>`` when the requested binary is missing
13
+ (controlled by ``DRYHACK_BROWSER_AUTO_INSTALL``, default on). Playwright is
14
+ still imported lazily and, if it is somehow absent, the tool returns an
15
+ actionable install hint instead of crashing.
13
16
 
14
17
  Playwright's sync API cannot run inside a live asyncio event loop (the MCP
15
18
  server runs one), so every browser session is executed in a dedicated worker
@@ -424,6 +427,50 @@ def _build_context_options(session: Dict[str, Any]) -> Dict[str, Any]:
424
427
  return opts
425
428
 
426
429
 
430
+ def _auto_install_browser(browser_name: str) -> Tuple[bool, str]:
431
+ """Best-effort ``playwright install <browser>`` for the current interpreter.
432
+
433
+ The browser binaries are not shipped via pip, so the first time a given
434
+ engine is used we download it on demand. Returns (ok, log_tail).
435
+ """
436
+ import subprocess
437
+ import sys
438
+
439
+ try:
440
+ proc = subprocess.run(
441
+ [sys.executable, "-m", "playwright", "install", browser_name],
442
+ capture_output=True,
443
+ text=True,
444
+ timeout=config.BROWSER_INSTALL_TIMEOUT,
445
+ )
446
+ log = ((proc.stdout or "") + (proc.stderr or "")).strip()
447
+ return proc.returncode == 0, log[-1500:]
448
+ except Exception as exc: # noqa: BLE001 - report, don't crash
449
+ return False, f"{type(exc).__name__}: {exc}"
450
+
451
+
452
+ def _launch_browser(browser_type, browser_name: str, launch_kwargs: Dict[str, Any]):
453
+ """Launch the browser, auto-installing its binary on first use if missing."""
454
+ try:
455
+ return browser_type.launch(**launch_kwargs)
456
+ except Exception as exc: # noqa: BLE001
457
+ msg = str(exc)
458
+ binary_missing = (
459
+ "Executable doesn't exist" in msg
460
+ or "playwright install" in msg.lower()
461
+ or "please run the following command" in msg.lower()
462
+ )
463
+ if binary_missing and config.BROWSER_AUTO_INSTALL:
464
+ ok, log = _auto_install_browser(browser_name)
465
+ if ok:
466
+ return browser_type.launch(**launch_kwargs)
467
+ raise RuntimeError(
468
+ f"{browser_name} binary missing and auto-install failed. "
469
+ f"Run `playwright install {browser_name}` manually.\n{log}"
470
+ ) from exc
471
+ raise
472
+
473
+
427
474
  def _run_session(
428
475
  steps: List[Dict[str, Any]],
429
476
  session: Dict[str, Any],
@@ -434,16 +481,19 @@ def _run_session(
434
481
  except ImportError:
435
482
  return (
436
483
  [],
437
- "playwright is not installed. Install the optional extra:\n"
438
- " pip install 'dryhack-mcp[browser]'\n"
439
- " playwright install",
484
+ "playwright is not importable (it ships with dryhack-mcp). "
485
+ "Reinstall with:\n"
486
+ " pip install --force-reinstall dryhack-mcp\n"
487
+ "or install directly:\n"
488
+ " pip install 'playwright>=1.40' && playwright install",
440
489
  )
441
490
 
442
491
  results: List[Dict[str, Any]] = []
443
492
  default_timeout = session.get("timeout")
444
493
 
445
494
  with sync_playwright() as pw:
446
- browser_type = getattr(pw, session.get("browser_type", "chromium"))
495
+ browser_name = session.get("browser_type", "chromium")
496
+ browser_type = getattr(pw, browser_name)
447
497
  launch_kwargs: Dict[str, Any] = {"headless": session.get("headless", True)}
448
498
  if session.get("slow_mo") is not None:
449
499
  launch_kwargs["slow_mo"] = session["slow_mo"]
@@ -454,7 +504,7 @@ def _run_session(
454
504
  if session.get("proxy") is not None:
455
505
  launch_kwargs["proxy"] = session["proxy"]
456
506
 
457
- browser = browser_type.launch(**launch_kwargs)
507
+ browser = _launch_browser(browser_type, browser_name, launch_kwargs)
458
508
  try:
459
509
  context = browser.new_context(**_build_context_options(session))
460
510
  if default_timeout is not None:
@@ -510,8 +560,11 @@ def _run_session_threaded(
510
560
 
511
561
  thread = threading.Thread(target=_worker, daemon=True)
512
562
  thread.start()
513
- # Generous ceiling so a per-step Playwright timeout fires first.
514
- thread.join(timeout=(session.get("timeout") or config.COMMAND_TIMEOUT * 1000) / 1000 + 60)
563
+ # Generous ceiling so a per-step Playwright timeout fires first. The extra
564
+ # allowance covers a possible first-use `playwright install` download.
565
+ base = (session.get("timeout") or config.COMMAND_TIMEOUT * 1000) / 1000
566
+ install_allowance = config.BROWSER_INSTALL_TIMEOUT if config.BROWSER_AUTO_INSTALL else 60
567
+ thread.join(timeout=base + install_allowance)
515
568
  if thread.is_alive():
516
569
  return [], "browser session exceeded the overall time budget"
517
570
  return box.get("results", []), box.get("error")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dryhack-mcp
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing.
5
5
  Author: Afrizal F.A
6
6
  License: MIT License
@@ -36,6 +36,7 @@ Requires-Python: >=3.9
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
38
  Requires-Dist: mcp<2,>=1.2.0
39
+ Requires-Dist: playwright>=1.40
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: pytest>=7.0; extra == "dev"
41
42
  Requires-Dist: ruff>=0.1; extra == "dev"
@@ -88,16 +89,21 @@ python3 -m pip install -e ".[dev]"
88
89
 
89
90
  ### Browser tool (Playwright)
90
91
 
91
- The `browser` tool needs the optional `browser` extra plus the Playwright
92
- browser binaries:
92
+ Playwright is a **core dependency**, so a plain `pip install dryhack-mcp` /
93
+ `uvx dryhack-mcp` already ships it — no extra needed. The browser *binaries*
94
+ (Chromium/Firefox/WebKit) are not distributed via pip, so the first time the
95
+ `browser` tool launches an engine it **auto-runs `playwright install <browser>`**
96
+ if the binary is missing.
97
+
98
+ To pre-install the binaries (optional, avoids the one-time first-use download):
93
99
 
94
100
  ```bash
95
- python3 -m pip install 'dryhack-mcp[browser]'
96
- playwright install # or: playwright install chromium
101
+ playwright install # all engines
102
+ playwright install chromium # just chromium
97
103
  ```
98
104
 
99
- If Playwright is not installed, the `browser` tool returns an install hint
100
- instead of failing — the rest of the server keeps working.
105
+ Auto-install is controlled by `DRYHACK_BROWSER_AUTO_INSTALL` (default on); set it
106
+ to `0` to disable and manage browsers yourself.
101
107
 
102
108
  ## Run
103
109
 
@@ -181,6 +187,8 @@ Start the server separately with `dryhack-mcp --transport http`.
181
187
  | `DRYHACK_COMMAND_TIMEOUT` | `120` | Per-command timeout (seconds) |
182
188
  | `DRYHACK_WORKDIR` | cwd | Working directory for commands |
183
189
  | `DRYHACK_OUTPUT_LIMIT` | `65536` | Max stdout/stderr bytes captured |
190
+ | `DRYHACK_BROWSER_AUTO_INSTALL` | `1` | Auto-download the browser binary on first use (`browser` tool) |
191
+ | `DRYHACK_BROWSER_INSTALL_TIMEOUT` | `900` | Timeout (s) for the on-demand `playwright install` download |
184
192
  | `DRYHACK_HTTP_HOST` | `127.0.0.1` | Default HTTP bind host |
185
193
  | `DRYHACK_HTTP_PORT` | `8000` | Default HTTP bind port |
186
194
 
@@ -236,9 +244,11 @@ browser(action="goto", url="https://target.example/",
236
244
  init_script="Object.defineProperty(navigator,'webdriver',{get:()=>false})")
237
245
  ```
238
246
 
239
- Playwright runs in a dedicated worker thread (the MCP server owns the asyncio
240
- loop, and Playwright's sync API cannot run inside a live loop). The same
241
- authorized-use scope rules as the other tools apply.
247
+ Playwright ships with dryhack-mcp and runs in a dedicated worker thread (the MCP
248
+ server owns the asyncio loop, and Playwright's sync API cannot run inside a live
249
+ loop). The browser binary auto-installs on first use if missing
250
+ (`DRYHACK_BROWSER_AUTO_INSTALL`, default on). The same authorized-use scope
251
+ rules as the other tools apply.
242
252
 
243
253
  ## authorize (per-call scope)
244
254
 
@@ -1,4 +1,5 @@
1
1
  mcp<2,>=1.2.0
2
+ playwright>=1.40
2
3
 
3
4
  [browser]
4
5
  playwright>=1.40
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dryhack-mcp"
7
- version = "2.2.0"
7
+ version = "2.3.0"
8
8
  description = "DryHack-MCP - an MCP server providing offensive-security tooling (curl, python, shell, Playwright browser) and a scope-gated authorize engine for authorized penetration testing."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -19,10 +19,13 @@ classifiers = [
19
19
  ]
20
20
  dependencies = [
21
21
  "mcp>=1.2.0,<2",
22
+ "playwright>=1.40",
22
23
  ]
23
24
 
24
25
  [project.optional-dependencies]
25
26
  dev = ["pytest>=7.0", "ruff>=0.1"]
27
+ # Kept as a back-compat alias; Playwright is now a core dependency, so the
28
+ # `browser` tool works out of the box with a plain install.
26
29
  browser = ["playwright>=1.40"]
27
30
 
28
31
  [project.scripts]
@@ -36,8 +36,8 @@ class BrowserToolTests(unittest.TestCase):
36
36
  result = browser(action="goto", url="https://example.com")
37
37
  finally:
38
38
  builtins.__import__ = real_import
39
- self.assertIn("playwright is not installed", result)
40
- self.assertIn("dryhack-mcp[browser]", result)
39
+ self.assertIn("playwright is not importable", result)
40
+ self.assertIn("playwright", result.lower())
41
41
 
42
42
  def test_single_action_maps_convenience_params(self):
43
43
  captured = {}
@@ -93,5 +93,50 @@ class BrowserToolTests(unittest.TestCase):
93
93
  )
94
94
 
95
95
 
96
+ def test_launch_auto_installs_missing_binary_then_retries(self):
97
+ calls = {"launch": 0, "install": 0}
98
+
99
+ class FakeBrowserType:
100
+ def launch(self, **kwargs):
101
+ calls["launch"] += 1
102
+ if calls["launch"] == 1:
103
+ raise RuntimeError(
104
+ "Executable doesn't exist at /path/chrome. "
105
+ "Please run the following command: playwright install"
106
+ )
107
+ return "browser-handle"
108
+
109
+ def fake_install(browser_name):
110
+ calls["install"] += 1
111
+ return True, "ok"
112
+
113
+ original = browser_tool._auto_install_browser
114
+ browser_tool._auto_install_browser = fake_install
115
+ original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
116
+ browser_tool.config.BROWSER_AUTO_INSTALL = True
117
+ try:
118
+ handle = browser_tool._launch_browser(FakeBrowserType(), "chromium", {})
119
+ finally:
120
+ browser_tool._auto_install_browser = original
121
+ browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
122
+
123
+ self.assertEqual(handle, "browser-handle")
124
+ self.assertEqual(calls["launch"], 2) # failed once, retried once
125
+ self.assertEqual(calls["install"], 1)
126
+
127
+ def test_launch_does_not_autoinstall_when_disabled(self):
128
+ class FakeBrowserType:
129
+ def launch(self, **kwargs):
130
+ raise RuntimeError("Executable doesn't exist; playwright install")
131
+
132
+ original_flag = browser_tool.config.BROWSER_AUTO_INSTALL
133
+ browser_tool.config.BROWSER_AUTO_INSTALL = False
134
+ try:
135
+ with self.assertRaises(RuntimeError):
136
+ browser_tool._launch_browser(FakeBrowserType(), "chromium", {})
137
+ finally:
138
+ browser_tool.config.BROWSER_AUTO_INSTALL = original_flag
139
+
140
+
96
141
  if __name__ == "__main__":
97
142
  unittest.main()
File without changes
File without changes