pi-lean-dimension 0.2.4 → 0.3.1

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 (159) hide show
  1. package/README.md +1 -1
  2. package/node_modules/pi-lean-portal/AGENTS.md +15 -23
  3. package/node_modules/pi-lean-portal/README.md +35 -7
  4. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
  5. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1126
  6. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
  7. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
  8. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
  9. package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
  10. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
  11. package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
  12. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
  13. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
  15. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
  16. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
  17. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
  18. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
  19. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  20. package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
  21. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
  22. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  23. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  25. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  26. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
  27. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  28. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
  29. package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
  30. package/node_modules/pi-lean-portal/browser-toggle.ts +168 -385
  31. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
  32. package/node_modules/pi-lean-portal/contributed/README.md +65 -6
  33. package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
  34. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
  35. package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
  36. package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
  37. package/node_modules/pi-lean-portal/core/guides.ts +14 -18
  38. package/node_modules/pi-lean-portal/core/router.ts +2 -20
  39. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
  40. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
  41. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
  42. package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
  43. package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
  44. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
  45. package/node_modules/pi-lean-portal/index.ts +8 -1
  46. package/node_modules/pi-lean-portal/package.json +2 -1
  47. package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
  48. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
  49. package/node_modules/pi-lean-search/index.ts +134 -64
  50. package/node_modules/pi-lean-search/package.json +2 -1
  51. package/node_modules/playwright/README.md +3 -3
  52. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
  53. package/node_modules/playwright/lib/cli/reportActions.js +2 -2
  54. package/node_modules/playwright/lib/common/index.js +77 -52
  55. package/node_modules/playwright/lib/common/index.js.txt +10 -11
  56. package/node_modules/playwright/lib/errorContext.js +2 -3
  57. package/node_modules/playwright/lib/index.js +30 -8
  58. package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
  59. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
  60. package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
  61. package/node_modules/playwright/lib/program.js +19 -0
  62. package/node_modules/playwright/lib/runner/index.js +309 -153
  63. package/node_modules/playwright/lib/runner/index.js.txt +13 -12
  64. package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
  65. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
  66. package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
  67. package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
  68. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
  69. package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
  70. package/node_modules/playwright/lib/util.js +2 -29
  71. package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
  72. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
  73. package/node_modules/playwright/package.json +3 -6
  74. package/node_modules/playwright/types/test.d.ts +391 -4
  75. package/node_modules/playwright/types/testReporter.d.ts +79 -4
  76. package/node_modules/playwright-core/browsers.json +12 -18
  77. package/node_modules/playwright-core/lib/bootstrap.js +1 -1
  78. package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
  79. package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
  80. package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
  81. package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
  82. package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
  83. package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
  84. package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
  85. package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
  86. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  87. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  88. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  89. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  90. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  91. package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
  92. package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
  93. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
  94. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  95. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  96. package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
  97. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
  98. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
  99. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  100. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  101. package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  102. package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  103. package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
  104. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
  105. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
  106. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  107. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
  108. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  109. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  110. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  111. package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
  112. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
  113. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  114. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
  115. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
  116. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  117. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
  118. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
  119. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  120. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  121. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  122. package/node_modules/playwright-core/lib/xdg-open +338 -137
  123. package/node_modules/playwright-core/package.json +2 -2
  124. package/node_modules/playwright-core/types/protocol.d.ts +249 -36
  125. package/node_modules/playwright-core/types/structs.d.ts +3 -1
  126. package/node_modules/playwright-core/types/types.d.ts +2009 -266
  127. package/package.json +3 -3
  128. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
  129. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
  130. package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
  131. package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
  132. package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
  133. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
  134. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
  135. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
  136. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
  137. package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
  138. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
  139. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
  140. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
  141. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
  142. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
  143. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
  144. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
  145. package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
  146. package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
  147. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
  148. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
  149. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
  150. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
  151. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
  152. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
  153. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
  154. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
  155. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
  156. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
  157. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
  158. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
  159. /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
@@ -1,525 +0,0 @@
1
- """
2
- BrowserBridge — base class for Python browser automation backends.
3
-
4
- Provides JSON-RPC command routing, session lifecycle, element caching,
5
- and a ``run()`` main loop. Subclasses override ``create_browser_session()``,
6
- ``create_browser_context()``, and the ``do_*`` operation methods to implement
7
- specific browser backends (e.g. Chromium via Playwright, Camoufox, etc.).
8
-
9
- Protocol
10
- --------
11
- All communication is JSON-RPC 2.0 over stdin/stdout with newline-delimited
12
- framing. See ``transport.py`` for details.
13
-
14
- Lifecycle
15
- ---------
16
- 1. The TypeScript ``PythonPluginAdapter`` spawns the Python process.
17
- 2. The bridge starts its ``run()`` loop, waiting for commands.
18
- 3. First command is typically ``browser.navigate``, which calls
19
- ``create_browser_session()`` (if no session exists yet for that taskId)
20
- and then navigates.
21
- 4. Subsequent commands use the existing session.
22
- 5. ``browser.cleanup`` closes the session for a taskId.
23
- 6. ``browser.shutdown`` (sent via cleanupAll) terminates the process.
24
-
25
- Subclass contract (all ``@abstractmethod``)
26
- --------------------------------------------
27
- * ``create_browser_session(task_id, config)`` — build a per-task session dict.
28
- * ``create_browser_context(config)`` — build an isolated BrowserContext.
29
- * ``do_navigate / do_snapshot / do_click / do_type / do_scroll / do_go_back
30
- / do_press / do_screenshot / do_get_console_messages / do_clear_console
31
- / do_evaluate / do_get_cookies / do_add_cookies / do_clear_cookies
32
- / do_get_storage_state`` — the 15 operation methods. Each returns a
33
- dict whose shape is documented at the dispatch site (``_h_*`` handlers).
34
- ``do_cleanup`` has a concrete default (calls ``close_browser_session``)
35
- and need not be overridden.
36
-
37
- Named profiles are fully handled by the TypeScript side via
38
- ``core/shared/storage-state.ts`` (disk persistence). The Python
39
- bridge receives ``storageState`` in the navigate request and applies
40
- it when creating a new BrowserContext — it does NOT track shared
41
- contexts across tasks.
42
- """
43
-
44
- import traceback
45
- from abc import ABC, abstractmethod
46
- from typing import Any, Optional
47
-
48
- from .transport import (
49
- read_request,
50
- write_response,
51
- make_success_response,
52
- make_error_response,
53
- make_parse_error,
54
- make_invalid_request,
55
- make_application_error,
56
- InvalidRequestError,
57
- METHOD_NOT_FOUND,
58
- INVALID_PARAMS,
59
- SESSION_ERROR,
60
- )
61
- from .accessibility import parse_snapshot, AriaParseResult
62
-
63
- # ─── Default timeout ──────────────────────────────────────────────────
64
-
65
- DEFAULT_NAVIGATION_TIMEOUT_MS: int = 30_000
66
- DEFAULT_INTERACTION_TIMEOUT_MS: int = 10_000
67
-
68
-
69
- class BrowserBridge(ABC):
70
- """Base class for Python browser automation bridges.
71
-
72
- Subclass this and override the abstract methods listed in the module
73
- docstring. Call ``run()`` to start the JSON-RPC command loop.
74
- """
75
-
76
- # ── Session storage ─────────────────────────────────────────
77
-
78
- #: Per-taskId session data: {task_id: {...}}.
79
- #: The dict contents are backend-specific (page, context, etc.).
80
- #: You can store whatever you need here.
81
- sessions: dict[str, dict[str, Any]]
82
-
83
- #: Per-taskId element cache: {task_id: AriaParseResult}.
84
- element_caches: dict[str, AriaParseResult]
85
-
86
- #: Whether the bridge is still running.
87
- _running: bool
88
-
89
- #: Plugin configuration dict forwarded from the TypeScript adapter via
90
- #: the ``browser.init`` RPC. Defaults to ``{}`` for bridges that never
91
- #: receive an init call (e.g. older adapters, or the shipped
92
- #: ``chromium-py``/``firefox-py`` when run standalone). Subclasses read
93
- #: engine-specific options from ``self.plugin_config.get("launch", {})``.
94
- _plugin_config: dict[str, Any]
95
-
96
- def __init__(self) -> None:
97
- self.sessions = {}
98
- self.element_caches = {}
99
- self._running = False
100
- self._plugin_config = {}
101
-
102
- # ── Plugin config (forwarded via browser.init) ───────────────
103
-
104
- @property
105
- def plugin_config(self) -> dict[str, Any]:
106
- """Return the plugin config dict forwarded from the TypeScript adapter.
107
-
108
- Populated by the ``browser.init`` RPC handler. Always returns a
109
- dict (empty when no init was received) so subclasses can safely
110
- call ``self.plugin_config.get("launch", {})``.
111
- """
112
- return self._plugin_config
113
-
114
- # ── Subclass hooks ──────────────────────────────────────────
115
-
116
- @abstractmethod
117
- def create_browser_session(self, task_id: str, config: dict[str, Any]) -> dict[str, Any]:
118
- """Create a new browser session for the given task.
119
-
120
- Must return a dict that will be stored in ``self.sessions[task_id]``.
121
- The dict is backend-specific (e.g. containing a Playwright page/context).
122
- """
123
-
124
- def close_browser_session(self, task_id: str) -> None:
125
- """Close and clean up the session for the given task.
126
-
127
- Default implementation removes the session from the dict. Override
128
- to close browser pages/contexts before removal.
129
- """
130
- self.sessions.pop(task_id, None)
131
- self.element_caches.pop(task_id, None)
132
-
133
- @abstractmethod
134
- def create_browser_context(self, config: dict[str, Any]) -> Any:
135
- """Create a new isolated BrowserContext for a task session.
136
-
137
- Each task gets its own BrowserContext with no sharing between
138
- tasks. Named profiles are handled by the TypeScript side via
139
- ``core/shared/storage-state.ts`` — the ``config`` may contain
140
- ``storageState`` to restore cookies and localStorage.
141
- """
142
-
143
- # ── Session helpers ─────────────────────────────────────────
144
-
145
- def get_session(self, task_id: str) -> Optional[dict[str, Any]]:
146
- """Get the session data for a task, or None."""
147
- return self.sessions.get(task_id)
148
-
149
- def require_session(self, task_id: str) -> dict[str, Any]:
150
- """Get the session for a task, raising SESSION_ERROR if absent."""
151
- session = self.get_session(task_id)
152
- if session is None:
153
- raise SessionNotFoundError(
154
- f"No active session for task '{task_id}'. "
155
- "Call browser.navigate first."
156
- )
157
- return session
158
-
159
- def ensure_session(self, task_id: str, config: Optional[dict[str, Any]] = None) -> dict[str, Any]:
160
- """Get or create a session for the given task."""
161
- session = self.get_session(task_id)
162
- if session is not None:
163
- return session
164
- new_session = self.create_browser_session(task_id, config or {})
165
- self.sessions[task_id] = new_session
166
- return new_session
167
-
168
- def get_element_cache(self, task_id: str) -> Optional[AriaParseResult]:
169
- """Get the cached element parse result for a task, or None."""
170
- return self.element_caches.get(task_id)
171
-
172
- def set_element_cache(self, task_id: str, result: AriaParseResult) -> None:
173
- """Store a parsed element cache for a task."""
174
- self.element_caches[task_id] = result
175
-
176
- # ── Operation stubs (override in subclasses) ────────────────
177
- #
178
- # The 15 ``do_*`` methods below are the operation contract. Each
179
- # returns a dict whose shape is enforced at the dispatch site
180
- # (``_h_*`` handlers in ``_DISPATCH``). ``do_cleanup`` has a concrete
181
- # default and is not abstract.
182
-
183
- @abstractmethod
184
- def do_navigate(
185
- self,
186
- task_id: str,
187
- url: str,
188
- timeout_ms: int = DEFAULT_NAVIGATION_TIMEOUT_MS,
189
- storageState: Optional[dict[str, Any]] = None,
190
- profileName: Optional[str] = None,
191
- profileMode: Optional[str] = None,
192
- ) -> dict[str, Any]:
193
- """Navigate the browser to a URL."""
194
-
195
- @abstractmethod
196
- def do_snapshot(self, task_id: str) -> dict[str, Any]:
197
- """Take an accessibility snapshot of the current page."""
198
-
199
- @abstractmethod
200
- def do_click(self, task_id: str, ref: str) -> dict[str, Any]:
201
- """Click an element by @e ref."""
202
-
203
- @abstractmethod
204
- def do_type(self, task_id: str, ref: str, text: str) -> dict[str, Any]:
205
- """Type text into an element by @e ref."""
206
-
207
- @abstractmethod
208
- def do_scroll(self, task_id: str, direction: str) -> dict[str, Any]:
209
- """Scroll the page up or down."""
210
-
211
- @abstractmethod
212
- def do_go_back(self, task_id: str) -> dict[str, Any]:
213
- """Navigate back in history."""
214
-
215
- @abstractmethod
216
- def do_press(self, task_id: str, key: str) -> dict[str, Any]:
217
- """Press a keyboard key on the current page (or focused element)."""
218
-
219
- @abstractmethod
220
- def do_screenshot(self, task_id: str, full_page: bool = False) -> dict[str, Any]:
221
- """Take a screenshot of the current page (JPEG base64 data URI)."""
222
-
223
- @abstractmethod
224
- def do_get_console_messages(self, task_id: str) -> dict[str, Any]:
225
- """Get captured console messages."""
226
-
227
- @abstractmethod
228
- def do_clear_console(self, task_id: str) -> dict[str, Any]:
229
- """Clear captured console messages."""
230
-
231
- @abstractmethod
232
- def do_evaluate(
233
- self, task_id: str, expression: str, *, read_only: bool = False
234
- ) -> dict[str, Any]:
235
- """Evaluate JavaScript in the page."""
236
-
237
- @abstractmethod
238
- def do_get_cookies(
239
- self, task_id: str, urls: Optional[list[str]] = None
240
- ) -> dict[str, Any]:
241
- """Get all cookies, optionally filtered by URL."""
242
-
243
- @abstractmethod
244
- def do_add_cookies(
245
- self, task_id: str, cookies: list[dict[str, Any]]
246
- ) -> dict[str, Any]:
247
- """Add cookies to the browser context."""
248
-
249
- @abstractmethod
250
- def do_clear_cookies(
251
- self,
252
- task_id: str,
253
- name: Optional[str] = None,
254
- domain: Optional[str] = None,
255
- path: Optional[str] = None,
256
- ) -> dict[str, Any]:
257
- """Clear cookies, optionally filtered by name/domain/path."""
258
-
259
- @abstractmethod
260
- def do_get_storage_state(self, task_id: str) -> dict[str, Any]:
261
- """Get full storage state (cookies + localStorage + IndexedDB)."""
262
-
263
- def do_cleanup(self, task_id: str) -> dict[str, Any]:
264
- """Clean up resources for a specific task.
265
-
266
- Profile persistence is handled by the TypeScript side
267
- (``python-adapter.ts`` auto-saves storage state before calling
268
- cleanup), so this default always calls ``close_browser_session()``.
269
- """
270
- self.close_browser_session(task_id)
271
- return {"success": True}
272
-
273
- # ── Command handlers ────────────────────────────────────────
274
- #
275
- # One ``_h_*`` method per JSON-RPC method. Each extracts params and
276
- # calls the matching ``do_*`` method. ``_DISPATCH`` (class attribute,
277
- # built at the bottom of the class body) maps method name → handler.
278
-
279
- def _h_ping(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
280
- return make_success_response(cmd_id, "pong")
281
-
282
- def _h_init(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
283
- # Forward plugin config from the TypeScript adapter. Sent exactly
284
- # once after the ping handshake, before any other RPC.
285
- self._plugin_config = params.get("config") or {}
286
- return make_success_response(cmd_id, {"ok": True})
287
-
288
- def _h_shutdown(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
289
- self._running = False
290
- return make_success_response(cmd_id, "shutting_down")
291
-
292
- def _h_navigate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
293
- url = self._require_param(params, "url", str)
294
- task_id = self._require_param(params, "taskId", str)
295
- timeout_ms = params.get("timeoutMs", DEFAULT_NAVIGATION_TIMEOUT_MS)
296
- result = self.do_navigate(
297
- task_id, url, timeout_ms,
298
- storageState=params.get("storageState"),
299
- profileName=params.get("profileName"),
300
- profileMode=params.get("profileMode"),
301
- )
302
- return make_success_response(cmd_id, result)
303
-
304
- def _h_snapshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
305
- task_id = self._require_param(params, "taskId", str)
306
- return make_success_response(cmd_id, self.do_snapshot(task_id))
307
-
308
- def _h_click(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
309
- task_id = self._require_param(params, "taskId", str)
310
- ref = self._require_param(params, "ref", str)
311
- return make_success_response(cmd_id, self.do_click(task_id, ref))
312
-
313
- def _h_type(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
314
- task_id = self._require_param(params, "taskId", str)
315
- ref = self._require_param(params, "ref", str)
316
- text = self._require_param(params, "text", str)
317
- return make_success_response(cmd_id, self.do_type(task_id, ref, text))
318
-
319
- def _h_scroll(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
320
- task_id = self._require_param(params, "taskId", str)
321
- direction = self._require_param(params, "direction", str)
322
- if direction not in ("up", "down"):
323
- return make_error_response(
324
- cmd_id, INVALID_PARAMS,
325
- 'direction must be "up" or "down"',
326
- )
327
- return make_success_response(cmd_id, self.do_scroll(task_id, direction))
328
-
329
- def _h_go_back(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
330
- task_id = self._require_param(params, "taskId", str)
331
- return make_success_response(cmd_id, self.do_go_back(task_id))
332
-
333
- def _h_press(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
334
- task_id = self._require_param(params, "taskId", str)
335
- key = self._require_param(params, "key", str)
336
- return make_success_response(cmd_id, self.do_press(task_id, key))
337
-
338
- def _h_screenshot(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
339
- task_id = self._require_param(params, "taskId", str)
340
- full_page = params.get("fullPage", False)
341
- return make_success_response(cmd_id, self.do_screenshot(task_id, full_page))
342
-
343
- def _h_get_console_messages(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
344
- task_id = self._require_param(params, "taskId", str)
345
- return make_success_response(cmd_id, self.do_get_console_messages(task_id))
346
-
347
- def _h_clear_console(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
348
- task_id = self._require_param(params, "taskId", str)
349
- return make_success_response(cmd_id, self.do_clear_console(task_id))
350
-
351
- def _h_evaluate(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
352
- task_id = self._require_param(params, "taskId", str)
353
- expression = self._require_param(params, "expression", str)
354
- read_only = bool(params.get("readOnly", False))
355
- result = self.do_evaluate(task_id, expression, read_only=read_only)
356
- return make_success_response(cmd_id, result)
357
-
358
- def _h_get_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
359
- task_id = self._require_param(params, "taskId", str)
360
- return make_success_response(cmd_id, self.do_get_cookies(task_id, params.get("urls")))
361
-
362
- def _h_add_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
363
- task_id = self._require_param(params, "taskId", str)
364
- cookies = self._require_param(params, "cookies", list)
365
- return make_success_response(cmd_id, self.do_add_cookies(task_id, cookies))
366
-
367
- def _h_clear_cookies(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
368
- task_id = self._require_param(params, "taskId", str)
369
- # No required params beyond taskId — empty call clears ALL cookies
370
- result = self.do_clear_cookies(
371
- task_id, params.get("name"), params.get("domain"), params.get("path")
372
- )
373
- return make_success_response(cmd_id, result)
374
-
375
- def _h_get_storage_state(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
376
- task_id = self._require_param(params, "taskId", str)
377
- return make_success_response(cmd_id, self.do_get_storage_state(task_id))
378
-
379
- def _h_cleanup(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
380
- task_id = self._require_param(params, "taskId", str)
381
- return make_success_response(cmd_id, self.do_cleanup(task_id))
382
-
383
- def _h_describe_quirks(self, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
384
- # Return the bridge's declared quirks flags. Uses getattr with
385
- # defaults so a bare BrowserBridge (no Playwright quirks) returns
386
- # all-defaults rather than raising AttributeError.
387
- return make_success_response(cmd_id, {
388
- "fingerprint_managed_context": getattr(
389
- self, "_fingerprint_managed_context", False
390
- ),
391
- "eval_prefix": getattr(self, "_eval_prefix", ""),
392
- "scroll_via_wheel": getattr(self, "_scroll_via_wheel", False),
393
- "skip_default_viewport": getattr(self, "_skip_default_viewport", False),
394
- "skip_networkidle": getattr(self, "_skip_networkidle", False),
395
- "wrap_mw_eval_in_eval": getattr(self, "_wrap_mw_eval_in_eval", False),
396
- "csp_safe_readonly_via_init_script": getattr(
397
- self, "_csp_safe_readonly_via_init_script", False
398
- ),
399
- })
400
-
401
- #: JSON-RPC method name → handler. Built after the handlers are
402
- #: defined so the names are in scope. ``handle_command`` looks up
403
- #: here and calls ``handler(self, params, cmd_id)``.
404
- _DISPATCH = {
405
- "ping": _h_ping,
406
- "browser.init": _h_init,
407
- "shutdown": _h_shutdown,
408
- "browser.navigate": _h_navigate,
409
- "browser.snapshot": _h_snapshot,
410
- "browser.click": _h_click,
411
- "browser.type": _h_type,
412
- "browser.scroll": _h_scroll,
413
- "browser.goBack": _h_go_back,
414
- "browser.press": _h_press,
415
- "browser.screenshot": _h_screenshot,
416
- "browser.getConsoleMessages": _h_get_console_messages,
417
- "browser.clearConsole": _h_clear_console,
418
- "browser.evaluate": _h_evaluate,
419
- "browser.getCookies": _h_get_cookies,
420
- "browser.addCookies": _h_add_cookies,
421
- "browser.clearCookies": _h_clear_cookies,
422
- "browser.getStorageState": _h_get_storage_state,
423
- "browser.cleanup": _h_cleanup,
424
- "browser.describeQuirks": _h_describe_quirks,
425
- }
426
-
427
- # ── Command routing ─────────────────────────────────────────
428
-
429
- def handle_command(self, method: str, params: dict[str, Any], cmd_id: Any) -> dict[str, Any]:
430
- """Route a JSON-RPC method to the appropriate operation handler.
431
-
432
- Returns a JSON-RPC response dict (either result or error).
433
- """
434
- try:
435
- handler = self._DISPATCH.get(method)
436
- if handler is None:
437
- return make_error_response(
438
- cmd_id, METHOD_NOT_FOUND, f"Method not found: {method}"
439
- )
440
- return handler(self, params, cmd_id)
441
-
442
- except SessionNotFoundError as exc:
443
- return make_error_response(cmd_id, SESSION_ERROR, str(exc))
444
- except NotImplementedError as exc:
445
- return make_application_error(cmd_id, str(exc))
446
- except Exception as exc:
447
- tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
448
- return make_application_error(cmd_id, str(exc), traceback_str=tb)
449
-
450
- # ── Main loop ─────────────────────────────────────────────
451
-
452
- def run(self) -> None:
453
- """Start the main JSON-RPC command loop.
454
-
455
- Reads requests from stdin, dispatches them, and writes responses
456
- to stdout. Runs until EOF or a ``shutdown`` command.
457
- """
458
- self._running = True
459
- while self._running:
460
- try:
461
- request = read_request()
462
- if request is None:
463
- break # EOF
464
-
465
- cmd_id = request.get("id")
466
- method = request.get("method", "")
467
- params = request.get("params", {})
468
-
469
- if not isinstance(params, dict):
470
- write_response(make_error_response(
471
- cmd_id, INVALID_PARAMS,
472
- '"params" must be a JSON object',
473
- ))
474
- continue
475
-
476
- response = self.handle_command(method, params, cmd_id)
477
- write_response(response)
478
-
479
- except InvalidRequestError:
480
- # Valid JSON but not a valid JSON-RPC Request object
481
- write_response(make_invalid_request(None))
482
- except ValueError:
483
- # JSON parse error
484
- write_response(make_parse_error(None))
485
- except EOFError:
486
- break
487
- except KeyboardInterrupt:
488
- break
489
- except Exception as exc:
490
- tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
491
- write_response(make_application_error(None, str(exc), traceback_str=tb))
492
-
493
- # ── Internal helpers ──────────────────────────────────────
494
-
495
- @staticmethod
496
- def _require_param(
497
- params: dict[str, Any],
498
- key: str,
499
- expected_type: type,
500
- ) -> Any:
501
- """Require a param to exist and be of the expected type.
502
-
503
- Raises ``InvalidParamsError`` if the param is missing or has the
504
- wrong type. The caller's ``except Exception`` clause turns this
505
- into a JSON-RPC application-error response.
506
- """
507
- if key not in params:
508
- raise InvalidParamsError(f'Missing required parameter: "{key}"')
509
- value = params[key]
510
- if not isinstance(value, expected_type):
511
- raise InvalidParamsError(
512
- f'Parameter "{key}" must be of type {expected_type.__name__}, '
513
- f"got {type(value).__name__}"
514
- )
515
- return value
516
-
517
-
518
- # ─── Custom exceptions ────────────────────────────────────────────────
519
-
520
- class SessionNotFoundError(Exception):
521
- """Raised when an operation requires a session but none exists."""
522
-
523
-
524
- class InvalidParamsError(Exception):
525
- """Raised when a required parameter is missing or has the wrong type."""
@@ -1,164 +0,0 @@
1
- /**
2
- * URL Safety Checks
3
- *
4
- * Prevents SSRF (Server-Side Request Forgery) and secret exfiltration
5
- * by validating URLs before navigation.
6
- *
7
- * Patterns from hermes-agent's url_safety.py.
8
- */
9
-
10
- // ─── Blocked hostnames ───────────────────────────────────────────────
11
-
12
- /** Internal/private network ranges that should never be accessed */
13
- const BLOCKED_HOSTNAMES = [
14
- "localhost",
15
- "127.0.0.1",
16
- "0.0.0.0",
17
- "::1",
18
- "169.254.169.254", // AWS/GCP metadata endpoint
19
- "metadata.google.internal",
20
- "100.100.100.200", // Alibaba Cloud metadata
21
- ];
22
-
23
- /** Regex for private IP ranges (10.x, 172.16-31.x, 192.168.x) */
24
- const PRIVATE_IP_REGEX = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/;
25
-
26
- // ─── Blocked URL patterns ─────────────────────────────────────────────
27
-
28
- /** File-like schemes that shouldn't be followed */
29
- const BLOCKED_SCHEMES = ["file:", "ftp:", "data:", "javascript:", "vbscript:"];
30
-
31
- // ─── Secret detection ────────────────────────────────────────────────
32
-
33
- /**
34
- * Patterns that look like API keys, tokens, or passwords in URLs.
35
- * Follows hermes-agent's approach: checks for known key prefixes rather
36
- * than generic length-based patterns, reducing false positives.
37
- * Also checks the percent-decoded URL for encoded secrets.
38
- */
39
- const SECRET_PATTERNS = [
40
- // Generic API keys in query params (specific param names)
41
- /[?&](api[_-]?key|token|secret|password|passwd|auth|credential|private[_-]?key|access[_-]?key)=/i,
42
- // Bearer tokens in headers (unlikely in URLs but check anyway)
43
- /Bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/i,
44
- // GitHub tokens: ghp_, ghs_, ghg_, gho_, ghu_
45
- /gh[psogu]_[A-Za-z0-9_]{36}/,
46
- // Slack tokens: xoxb-, xoxp-, xoxa-, xoxs-, xoxr-
47
- /xox[bpras]-[A-Za-z0-9-]+/,
48
- // Stripe: sk_live_, sk_test_, pk_live_, pk_test_, whsec_
49
- /(sk|pk)_(live|test)_[A-Za-z0-9]+/,
50
- /whsec_[A-Za-z0-9]+/,
51
- // AWS access key ID (starts with AKIA)
52
- /AKIA[A-Z0-9]{16}/,
53
- // Google API keys (AIza prefix)
54
- /AIza[0-9A-Za-z\-_]{35}/,
55
- // Discord bot tokens
56
- /[MN][A-Za-z\d]{23}\.[XZ][A-Za-z\d]{6}\.[A-Za-z\d]{27}/,
57
- // SendGrid API keys (SG. prefix)
58
- /SG\.[A-Za-z0-9-_]{22}\.[A-Za-z0-9-_]{43}/,
59
- // HubSpot API keys
60
- /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/,
61
- // Generic secret in URL path segment (e.g., "secret=..." in path)
62
- /\/secret(?:s)?\//i,
63
- ];
64
-
65
- // ─── Types ────────────────────────────────────────────────────────────
66
-
67
- export interface UrlSafetyResult {
68
- /** Whether the URL is safe to navigate to */
69
- safe: boolean;
70
- /** Human-readable reason if not safe */
71
- reason?: string;
72
- /** The specific issue category */
73
- category?: "ssrf" | "scheme" | "secret" | "malformed";
74
- }
75
-
76
- // ─── Validation ───────────────────────────────────────────────────────
77
-
78
- export function validateUrl(rawUrl: string): UrlSafetyResult {
79
- let parsed: URL;
80
- try {
81
- parsed = new URL(rawUrl);
82
- } catch {
83
- return { safe: false, reason: "Malformed URL", category: "malformed" };
84
- }
85
-
86
- // Check scheme
87
- const scheme = parsed.protocol.toLowerCase();
88
- if (BLOCKED_SCHEMES.includes(scheme)) {
89
- return {
90
- safe: false,
91
- reason: `Blocked scheme: ${scheme}`,
92
- category: "scheme",
93
- };
94
- }
95
-
96
- if (scheme !== "http:" && scheme !== "https:") {
97
- return {
98
- safe: false,
99
- reason: `Unsupported scheme: ${scheme}. Only http and https are allowed.`,
100
- category: "scheme",
101
- };
102
- }
103
-
104
- // Check hostname against blocked list
105
- const hostname = parsed.hostname.toLowerCase();
106
- for (const blocked of BLOCKED_HOSTNAMES) {
107
- if (hostname === blocked || hostname.endsWith("." + blocked)) {
108
- return {
109
- safe: false,
110
- reason: `Blocked hostname: ${hostname} (private/internal network)`,
111
- category: "ssrf",
112
- };
113
- }
114
- }
115
-
116
- // Check for private IP ranges
117
- if (PRIVATE_IP_REGEX.test(hostname)) {
118
- return {
119
- safe: false,
120
- reason: `Blocked IP range: ${hostname} (private network)`,
121
- category: "ssrf",
122
- };
123
- }
124
-
125
- // Check for IPv6 loopback
126
- if (
127
- hostname.startsWith("[") &&
128
- (hostname.includes("::1") || hostname.includes("::ffff:127"))
129
- ) {
130
- return {
131
- safe: false,
132
- reason: `Blocked IPv6 address: ${hostname} (loopback)`,
133
- category: "ssrf",
134
- };
135
- }
136
-
137
- // Check for secrets in URL (raw and percent-decoded)
138
- const urlStr = parsed.toString();
139
- try {
140
- const decodedUrl = decodeURIComponent(urlStr);
141
- for (const pattern of SECRET_PATTERNS) {
142
- if (pattern.test(urlStr) || pattern.test(decodedUrl)) {
143
- return {
144
- safe: false,
145
- reason: `Potential secret detected in URL. Remove sensitive parameters before navigating.`,
146
- category: "secret",
147
- };
148
- }
149
- }
150
- } catch {
151
- // If decoding fails, just check the raw URL
152
- for (const pattern of SECRET_PATTERNS) {
153
- if (pattern.test(urlStr)) {
154
- return {
155
- safe: false,
156
- reason: `Potential secret detected in URL. Remove sensitive parameters before navigating.`,
157
- category: "secret",
158
- };
159
- }
160
- }
161
- }
162
-
163
- return { safe: true };
164
- }