pi-lean-dimension 0.1.0 → 0.2.0

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 (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
@@ -0,0 +1,304 @@
1
+ # Stealth Backends — Install Guide
2
+
3
+ Stealth backends are **user-installed browser plugins** that drive
4
+ patched/fingerprint-managed browser binaries (e.g. Camoufox) for sites
5
+ that block the shipped Chromium/Firefox. They are **never shipped in
6
+ the npm tarball** and the extension never downloads or executes them
7
+ automatically — you write or audit the bridge, you create the venv, you
8
+ fetch the binary, and you register it in `settings.json`.
9
+
10
+ This guide walks through the install flow with
11
+ [**Camoufox**](https://github.com/nichochar/camoufox) as the worked
12
+ example. The same shape applies to any stealth backend that subclasses
13
+ `PlaywrightBridge` using the [quirks schema](#quirks-schema-reference)
14
+ documented in `playwright_base.py`.
15
+
16
+ > **You need the git repo, not just `npm install pi-lean-portal`.**
17
+ > The template bridge and this README live under
18
+ > `packages/pi-lean-portal/contributed/` in the source
19
+ > repository. `docs/` is excluded from the portal `package.json`
20
+ > `files`, so the templates are not in the published tarball. Clone the
21
+ > repo (or obtain a copy of these files) before starting.
22
+
23
+ For *whether* you should reach for a stealth backend at all, see
24
+ [`CHOOSING.md`](./CHOOSING.md). For the architecture (multi-root
25
+ discovery, `browser.init` RPC, `PYTHONPATH` injection), see the
26
+ "Stealth backends (user-managed)" section of
27
+ [`packages/pi-lean-portal/AGENTS.md`](../../AGENTS.md).
28
+
29
+ ## Where things live
30
+
31
+ ```
32
+ ~/.pi/agent/pi-lean-portal/
33
+ ├── web-guides/ (existing)
34
+ ├── browser-state/ (existing)
35
+ └── user-backends/ ← stealth backends go here
36
+ └── camoufox-py/
37
+ ├── bridge.py ← you copy this from the source repo
38
+ └── .venv/ ← you create this (engine pip pkg + playwright)
39
+ ```
40
+
41
+ This is a **different tree from any in-repo test fixtures** (which live
42
+ gitignored under `bench/miniwob/fixtures/` for the evaluation harness).
43
+ The `~/.pi/agent/pi-lean-portal/user-backends/` tree is what the pi
44
+ agent's production `detectPluginType` reads at runtime. The two concerns
45
+ are deliberately separate.
46
+
47
+ ## Install flow (Camoufox)
48
+
49
+ ### 1. Pick a location
50
+
51
+ ```
52
+ ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py/
53
+ ```
54
+
55
+ The convention is `<name>-py/` (mirrors the shipped `chromium-py` /
56
+ `firefox-py` naming). The directory name is the plugin `name` you will
57
+ register in `settings.json`.
58
+
59
+ ### 2. Copy the template bridge
60
+
61
+ Copy the source-repo template into your user-backends tree:
62
+
63
+ ```bash
64
+ mkdir -p ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py
65
+ cp packages/pi-lean-portal/contributed/camoufox-py/bridge.py \
66
+ ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py/bridge.py
67
+ ```
68
+
69
+ You need the **git repo** for this step — the template is not in the
70
+ `npm install pi-lean-portal` tarball (`docs/` is excluded from
71
+ `package.json` `files` by design). Alternatively, write your own
72
+ subclass using the [quirks schema](#quirks-schema-reference) documented
73
+ in `backends/python-base/pi_browser_bridge/playwright_base.py`. Either
74
+ way, audit what you copy — this is trusted user code that the extension
75
+ will spawn as a subprocess.
76
+
77
+ ### 3. Create the venv and install dependencies
78
+
79
+ ```bash
80
+ cd ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py
81
+ python3 -m venv .venv
82
+ . .venv/bin/activate
83
+ pip install "cloverlabs-camoufox[geoip]" playwright
84
+ ```
85
+
86
+ You do **not** need to `pip install` the shared `pi_browser_bridge`
87
+ library — the `PythonPluginAdapter` injects the package's
88
+ `backends/python-base/` onto `PYTHONPATH` automatically at spawn time
89
+ (see `python-adapter.ts` `_buildPythonPath()`), so
90
+ `from pi_browser_bridge.playwright_base import PlaywrightBridge` works
91
+ from your venv without a PyPI package.
92
+
93
+ ### 4. Fetch the patched binary
94
+
95
+ Engine-specific. For Camoufox:
96
+
97
+ ```bash
98
+ . .venv/bin/activate
99
+ python -m camoufox fetch
100
+ ```
101
+
102
+ This downloads the patched Firefox binary (~100 MB). If it is missing
103
+ at runtime, the bridge's `_install_hint` surfaces the command in the
104
+ error so you know what to run.
105
+
106
+ ### 5. System dependencies
107
+
108
+ - **Linux + `headless='virtual'`**: install `xvfb`
109
+ (`apt install xvfb` / `dnf install xorgx11-server-Xvfb`). True
110
+ headless (`headless=True`, the bridge's default) works without xvfb.
111
+ - macOS / Windows: no extra system deps for the default headless mode.
112
+
113
+ ### 6. Register in `settings.json`
114
+
115
+ Edit `~/.pi/agent/settings.json` (global) or `.pi/settings.json`
116
+ (project-local). Add the plugin under `browser.plugins` with an
117
+ **absolute** `pythonPath` pointing at your venv's interpreter and a
118
+ `launch` object:
119
+
120
+ ```jsonc
121
+ {
122
+ "browser": {
123
+ "plugins": [
124
+ {
125
+ "name": "camoufox-py",
126
+ "dir": "camoufox-py",
127
+ "enabled": true,
128
+ "config": {
129
+ "pythonPath": "/home/me/.pi/agent/pi-lean-portal/user-backends/camoufox-py/.venv/bin/python",
130
+ "launch": {
131
+ "headless": true,
132
+ "os": "windows",
133
+ "humanize": true,
134
+ "enableCache": true,
135
+ "mainWorldEval": true
136
+ }
137
+ }
138
+ }
139
+ ]
140
+ }
141
+ }
142
+ ```
143
+
144
+ Notes:
145
+
146
+ - **`pythonPath` must be absolute.** A relative `pythonPath` is not
147
+ resolved against `USER_BACKENDS_DIR` (that nicety is intentionally
148
+ out of scope — see the stealth plan's "Out of scope" list). Point it
149
+ at `<user-backends>/camoufox-py/.venv/bin/python`.
150
+ - **`dir`** is resolved against the user-backends root (multi-root
151
+ discovery: package `backends/` → `USER_BACKENDS_DIR` → absolute). A
152
+ bare `"camoufox-py"` resolves to `~/.pi/agent/pi-lean-portal/
153
+ user-backends/camoufox-py/`. You may also pass an absolute `dir`.
154
+ - **`launch`** keys are forwarded to the bridge as `plugin_config.launch`
155
+ via the `browser.init` RPC. The Camoufox bridge reads them in
156
+ `_launch_browser()` and passes them to `camoufox.NewBrowser`. Defaults
157
+ if you omit them: `headless=true`, `os="windows"`, `geoip=true`,
158
+ `humanize=true`, `enableCache=true`, `mainWorldEval=true`.
159
+ - **Stealth backends are never in the default fallback list.** When
160
+ `browser.plugins` is absent, only the four shipped backends
161
+ (`chromium`, `firefox`, `chromium-py`, `firefox-py`) are loaded. A
162
+ fresh install must not emit validation errors for plugins the user
163
+ never asked for.
164
+
165
+ ### 7. Verify
166
+
167
+ 1. **`/web status`** — lists `camoufox-py` among the discovered plugins
168
+ (it appears once `settings.json` is picked up).
169
+ 2. **`browser-navigate` to a test page** — e.g. navigate to
170
+ `https://example.com`. The first navigate spawns the bridge, runs
171
+ the `ping` handshake, sends `browser.init`, launches Camoufox, and
172
+ returns an accessibility tree.
173
+ 3. **Missing binary** — if you skipped step 4, the navigate fails with
174
+ the bridge's `_install_hint` telling you to run
175
+ `python -m camoufox fetch`.
176
+
177
+ ### 8. Security note
178
+
179
+ `user-backends/` is **trusted user code**. The extension never
180
+ downloads, fetches, or executes stealth backends automatically — there
181
+ is no plugin marketplace and no auto-install. You wrote or audited
182
+ every line of `bridge.py`, you created the venv, and you fetched the
183
+ binary. Audit anything you copy from elsewhere before pointing the
184
+ agent at it; the bridge runs as a subprocess with whatever network and
185
+ filesystem access your user account has.
186
+
187
+ ### 9. Benchmarking (optional)
188
+
189
+ To run the full 130-task [MiniWoB++](https://miniwob.farama.org/) suite
190
+ against your Camoufox install (or any installed stealth backend),
191
+ use the contributed discovery runner at
192
+ `packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts`.
193
+ The runner discovers every backend under `user-backends/*-py/`, loads
194
+ config from the test-local `settings.json`, and runs contract +
195
+ persistence + parity suites for each — all forwarding your configured
196
+ `launch` options. Opt-in via `CONTRIB_RUN=1`:
197
+
198
+ ```bash
199
+ npm run setup:miniwob # one-time: clone MiniWoB++ content
200
+ CONTRIB_RUN=1 npx vitest run packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts
201
+ ```
202
+
203
+ **Single-backend isolation:** set `PI_USER_BACKENDS_DIR` at a temp root
204
+ containing only the backend you want to test:
205
+
206
+ ```bash
207
+ ln -s ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py /tmp/one-backend/
208
+ PI_USER_BACKENDS_DIR=/tmp/one-backend CONTRIB_RUN=1 \
209
+ npx vitest run packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts
210
+ ```
211
+
212
+ The runner uses `registerContributedParitySuite` from
213
+ `bench/miniwob/solvers/contributed-parity.ts`. See that file and the
214
+ [runner source](../../__tests__/run-contributed-suites.test.ts) for
215
+ the full prerequisites and the config-forwarding wiring shape if you
216
+ want to write your own benchmark against a different backend setup.
217
+
218
+ ## Quirks schema reference
219
+
220
+ The contract for writing your own stealth backend is the set of class
221
+ attributes on `PlaywrightBridge` in
222
+ `backends/python-base/pi_browser_bridge/playwright_base.py`. Set them
223
+ as class attributes on your subclass:
224
+
225
+ | Flag | Default | Effect when set |
226
+ |------|---------|-----------------|
227
+ | `_fingerprint_managed_context` | `False` | `create_browser_context()` skips hardcoded `viewport`/`user_agent`; lets the fingerprint package set them. |
228
+ | `_eval_prefix` | `""` | Prepended to every `page.evaluate` expression in `do_evaluate` (e.g. Camoufox's `"mw:"` routes writes to the main world). |
229
+ | `_scroll_via_wheel` | `False` | `do_scroll` uses `page.mouse.wheel` instead of `page.evaluate("window.scrollBy")` (avoids eval-write under isolated-world stealth). |
230
+ | `_skip_default_viewport` | `False` | Skips Playwright's `Browser.setDefaultViewport` CDP call (Camoufox binary rejects its `isMobile` prop). |
231
+ | `_skip_networkidle` | `False` | Nav-settle uses `load` instead of `networkidle` (patched binaries don't fire `networkidle` reliably). |
232
+ | `_wrap_mw_eval_in_eval` | `False` | `do_evaluate` rewrites the expression as `eval(<JSON-string of expression>)` before prepending `_eval_prefix`, so multi-statement scripts survive Camoufox's `let _s = (${script})` main-world wrapper (which only accepts a single expression). Camoufox-only; flip back to `False` when a future driver fixes the wrapper. |
233
+
234
+ All flags default off, so the shipped `chromium-py` / `firefox-py`
235
+ behavior is bit-identical to a pre-stealth install. For the two
236
+ lifecycle patterns (engine-accepts-external-Playwright vs.
237
+ engine-owns-its-own-Playwright) and the trade-offs of writing your own,
238
+ see [`CHOOSING.md`](./CHOOSING.md).
239
+
240
+ ## Test-local settings for contributed runners
241
+
242
+ The contributed-backend runner (`run-contributed-suites.test.ts`) reads
243
+ backend configuration from a local settings file at:
244
+
245
+ ```
246
+ packages/pi-lean-portal/__tests__/contributed/settings.json
247
+ ```
248
+
249
+ This file is gitignored — you create it locally. Below is a sample with
250
+ every supported field explained:
251
+
252
+ ```json
253
+ {
254
+ "$comment": "Sample test-local settings for contributed-backend runner. Copy to packages/pi-lean-portal/__tests__/contributed/settings.json and adjust for your installed backend.",
255
+ "browser": {
256
+ "plugins": [
257
+ {
258
+ "name": "camoufox-py",
259
+ "dir": "camoufox-py",
260
+ "enabled": true,
261
+ "config": {
262
+ "pythonPath": "/absolute/path/to/camoufox-py/.venv/bin/python3",
263
+ "capabilities": {
264
+ "engine": "firefox",
265
+ "supportsFullPageScreenshot": true,
266
+ "supportsJavaScriptEvaluate": true
267
+ },
268
+ "transportTimeoutMs": 60000,
269
+ "launch": {
270
+ "headless": true,
271
+ "humanize": false,
272
+ "os": "windows",
273
+ "geoip": false
274
+ }
275
+ }
276
+ }
277
+ ]
278
+ }
279
+ }
280
+ ```
281
+
282
+ | Field | Purpose |
283
+ |-------|---------|
284
+ | `name` | Plugin name, must match the directory under `user-backends/` (e.g. `camoufox-py`). |
285
+ | `dir` | Directory name, resolved against the user-backends root. A bare `"camoufox-py"` resolves to `~/.pi/agent/pi-lean-portal/user-backends/camoufox-py/`. |
286
+ | `enabled` | Set to `true` for the runner to pick up this backend. |
287
+ | `pythonPath` | **Absolute** path to the venv's Python interpreter. If omitted, the runner auto-detects `probe.venvPython` from the user-backend probe. |
288
+ | `capabilities` | Override the backend's capability flags. Fields not listed inherit defaults from `DEFAULT_CAPABILITIES`. |
289
+ | `capabilities.engine` | Browser engine identifier: `"firefox"` or `"chromium"`. Controls capability resolution. |
290
+ | `capabilities.supportsFullPageScreenshot` | Whether the backend can capture full-page screenshots. |
291
+ | `capabilities.supportsJavaScriptEvaluate` | Whether the backend supports `page.evaluate`. |
292
+ | `transportTimeoutMs` | JSON-RPC transport timeout in milliseconds. Defaults to the adapter's built-in fallback (usually 30s). |
293
+ | `launch` | Options forwarded to the bridge as `plugin_config.launch` via the `browser.init` RPC. |
294
+ | `launch.headless` | Run browser headless (`true`) or with a visible window (`false`). |
295
+ | `launch.humanize` | Add human-like mouse/timing noise to evade bot detection. |
296
+ | `launch.os` | Spoofed OS identity: `"windows"`, `"macos"`, or `"linux"`. |
297
+ | `launch.geoip` | Enable GeoIP-based locale/language spoofing. |
298
+
299
+ Override the settings path with `CONTRIB_SETTINGS`:
300
+
301
+ ```bash
302
+ CONTRIB_SETTINGS=/path/to/my-settings.json CONTRIB_RUN=1 \
303
+ npx vitest run packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts
304
+ ```
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Camoufox-Py Bridge — Python-side stealth backend using Camoufox.
4
+ User-installed backend for fingerprint-managed browsing via cloverlabs-camoufox.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+
10
+ # Lazy-patch Playwright's coreBundle.js if needed. The patcher is idempotent
11
+ # and guarded: it only runs once per file (marker comment), warns on pattern
12
+ # miss, and raises on read-only filesystems with a manual fallback command.
13
+ try:
14
+ from pi_browser_bridge.patch_playwright import patch_playwright
15
+
16
+ # Auto-patch at module import time (before any navigate), so the driver
17
+ # is fixed before any pageerror can crash it. Loud = one-time stderr
18
+ # notice on first patch. fail_on_readonly=True means we raise early
19
+ # with a clear instruction if the filesystem is read-only.
20
+ patch_playwright(loud=True, fail_on_readonly=True)
21
+ except ImportError:
22
+ # The patcher module lives in the portal's python-base lib; if it's not
23
+ # importable (e.g. PYTHONPATH not set, or the user is running from a
24
+ # standalone venv without the pi_browser_bridge package), silently
25
+ # continue. The crash will manifest when a JS page fires uncaught
26
+ # errors, and the user can manually patch via:
27
+ # python -m pi_browser_bridge.patch_playwright
28
+ pass
29
+
30
+ from pi_browser_bridge.playwright_base import PlaywrightBridge
31
+
32
+
33
+ class CamoufoxPyBridge(PlaywrightBridge):
34
+ """Stealth bridge using Camoufox (Firefox-based).
35
+
36
+ The base's quirks dispatch handles eval-world routing (``mw:`` prefix)
37
+ and wheel-based scrolling (``_scroll_via_wheel``). This subclass sets
38
+ the quirk flags and overrides ``_launch_browser`` to call
39
+ ``camoufox.NewBrowser`` with the plugin config's launch options.
40
+
41
+ Fingerprint injection happens at **browser launch** via Camoufox's
42
+ patched binary and its ``env`` / ``firefox_user_prefs`` — not at
43
+ context creation. ``camoufox.NewContext`` exists on
44
+ ``cloverlabs-camoufox>=0.6.0`` but is broken on this binary (same
45
+ ``isMobile`` rejection), so context creation uses the standard
46
+ ``browser.new_context()``.
47
+ ``_fingerprint_managed_context = True`` prevents the base from
48
+ clobbering viewport/UA.
49
+
50
+ Back navigation (``/web back``) works via the base class
51
+ ``do_go_back()`` thanks to the ``enable_cache=True`` default, which
52
+ restores the session history that Camoufox disables by default.
53
+ """
54
+
55
+ _plugin_name: str = "camoufox-py"
56
+ _fingerprint_managed_context: bool = True
57
+ # Camoufox injects the fingerprint at **browser launch** via
58
+ # ``camoufox.NewBrowser`` — not at context creation.
59
+ # ``camoufox.NewContext`` exists on ``cloverlabs-camoufox>=0.6.0`` but
60
+ # crashes on this binary (same ``isMobile`` rejection). Keep using
61
+ # the standard ``browser.new_context()``.
62
+ _eval_prefix: str = "mw:"
63
+ _scroll_via_wheel: bool = True
64
+ # The Camoufox patched Firefox binary rejects the ``isMobile`` property that
65
+ # Playwright includes in ``Browser.setDefaultViewport``. Skip the call.
66
+ _skip_default_viewport: bool = True
67
+ # The patched Camoufox Firefox binary doesn't fire `networkidle` reliably;
68
+ # waiting for it in `do_go_back` / `_wait_for_page_ready` either times out
69
+ # or loiters in the Playwright sync greenlet long enough to risk deadlocking
70
+ # the Juggler driver. Match `do_navigate`'s load-based settle instead.
71
+ _skip_networkidle: bool = True
72
+ # The patched Camoufox Juggler main-world eval path wraps every `mw:`
73
+ # script as `let _s = (${script})`, which is a SyntaxError for any
74
+ # statement (let/var/multi-statement). Rewrite the script as
75
+ # `mw:eval(<json>)` so it is a single expression that `eval` runs
76
+ # verbatim. See the `_wrap_mw_eval_in_eval` quirk docstring.
77
+ _wrap_mw_eval_in_eval: bool = True
78
+ _install_hint: str = (
79
+ "Camoufox browser not installed.\n"
80
+ "Run the following commands in your camoufox-py virtual environment:\n"
81
+ " pip install cloverlabs-camoufox[geoip]\n"
82
+ " python -m camoufox fetch"
83
+ )
84
+
85
+ def _launch_browser(self): # type: ignore[override]
86
+ """Launch Camoufox via ``camoufox.NewBrowser``.
87
+
88
+ Reads launch options from the plugin config forwarded by
89
+ ``browser.init``. Applies sensible agent-friendly defaults
90
+ that produce a stable, human-looking persona (all overridable
91
+ via ``launch.*`` in ``settings.json``):
92
+
93
+ * ``os="windows"`` — stable persona; real humans don't switch
94
+ OS between sessions.
95
+ * ``geoip=True`` — match timezone / locale / geolocation to
96
+ the egress IP, avoiding the ``UTC`` bot tell.
97
+ * ``humanize=True`` — bezier-curved mouse motion (the "looks
98
+ human" knob; ~1.5s/click).
99
+ * ``enable_cache=True`` — restore session history (disabled by
100
+ Camoufox's stealth defaults), fixing ``/web back``.
101
+ * ``main_world_eval=True`` (always) — required for the ``mw:``
102
+ prefix to route ``page.evaluate`` writes to the main world.
103
+
104
+ Returns a standard Playwright ``Browser`` patched by Camoufox.
105
+ """
106
+ # Lazy-import so non-Camoufox bridges don't pay the import cost
107
+ # and to give a clear error when the package is missing.
108
+ try:
109
+ import camoufox # type: ignore[import-unresolved]
110
+ except ImportError as exc:
111
+ raise RuntimeError(self._install_hint) from exc
112
+
113
+ launch = self.plugin_config.get("launch", {}) or {}
114
+
115
+ # Build Camoufox NewBrowser kwargs from the plugin config.
116
+ # Map camelCase TypeScript keys to snake_case Python kwargs.
117
+ # Apply sensible defaults for a human-looking agent.
118
+ kwargs: dict[str, object] = {
119
+ # Default to true headless — works without xvfb on Linux servers.
120
+ # Users can override with false (headed debugging) or 'virtual'
121
+ # (xvfb-based high-coherence fingerprint).
122
+ "headless": launch.get("headless", True),
123
+ # Stable OS persona — a real human doesn't switch between
124
+ # Windows/Mac/Linux every session.
125
+ "os": launch.get("os", "windows"),
126
+ # Match timezone/locale/geolocation to the egress IP.
127
+ # Replaces the hardcoded UTC/en-US/no-geo with real values.
128
+ "geoip": launch.get("geoip", True),
129
+ # Bezier mouse motion (the "looks human" knob).
130
+ "humanize": launch.get("humanize", True),
131
+ # Restore session history disabled by Camoufox's stealth
132
+ # defaults. Without this, /web back silently no-ops.
133
+ "enable_cache": launch.get("enableCache", True),
134
+ }
135
+
136
+ _KEY_MAP: dict[str, str] = {
137
+ # Keys WITHOUT defaults above — pass-through only when present.
138
+ # (headless, os, geoip, humanize, enableCache are already handled
139
+ # by the defaults dict above via launch.get(..., default).)
140
+ "fingerprintPreset": "fingerprint_preset",
141
+ "executablePath": "executable_path",
142
+ "proxy": "proxy",
143
+ }
144
+ for ts_key, py_key in _KEY_MAP.items():
145
+ if ts_key in launch:
146
+ kwargs[py_key] = launch[ts_key]
147
+
148
+ # main_world_eval must be True so the mw: prefix works
149
+ kwargs["main_world_eval"] = launch.get("mainWorldEval", True)
150
+
151
+ # Redirect third-party stdout to stderr during launch.
152
+ # The camoufox package print()s to stdout (utils.py:154, addons.py:92,
153
+ # etc.) with no file=stderr — polluting the JSON-RPC wire. Swap
154
+ # sys.stdout → sys.stderr around the launch call so only
155
+ # transport.write_response (which runs later, in the RPC loop,
156
+ # with real stdout restored) ever writes to the real stdout.
157
+ # ponytail: launch-window only. If post-launch pollution appears
158
+ # (addon failures mid-session, inherited subprocess stdout), escalate
159
+ # to a process-wide redirect with write_response holding its own
160
+ # real_stdout ref captured at import.
161
+ real_stdout = sys.stdout
162
+ sys.stdout = sys.stderr
163
+ try:
164
+ return camoufox.NewBrowser(self._pw, **kwargs)
165
+ finally:
166
+ sys.stdout = real_stdout
167
+
168
+ # NOTE: do_go_back is intentionally NOT overridden.
169
+ #
170
+ # Back navigation relies on the base class ``do_go_back()``, which
171
+ # calls ``page.go_back(wait_until="load", timeout=15_000)`` (thanks
172
+ # to ``_skip_networkidle=True``). This works correctly when the
173
+ # ``enable_cache=True`` default is in effect, because it restores
174
+ # Camoufox's ``browser.sessionhistory.max_entries`` from 0 to 10.
175
+ #
176
+ # There is no need for a ``document.referrer`` workaround — some
177
+ # patched Firefox binaries have a binary-level back-navigation bug
178
+ # that requires that workaround; Camoufox does not, with
179
+ # ``enable_cache=True``.
180
+ #
181
+ # Testing: with ``enable_cache=True``, back navigation completes
182
+ # in ~0.04s and correctly returns the previous page URL/title/body.
183
+ # Without ``enable_cache``, ``page.go_back()`` returns in 0.00s
184
+ # as a silent no-op (URL unchanged). Users who explicitly set
185
+ # ``enableCache: false`` must accept this limitation.
186
+
187
+
188
+ # ═══════════════════════════════════════════════════════════════════════
189
+ # Entry point
190
+ # ═══════════════════════════════════════════════════════════════════════
191
+
192
+ if __name__ == "__main__":
193
+ # Print a JSON-RPC error and exit immediately if camoufox is missing,
194
+ # so the TypeScript PythonPluginAdapter gets a parseable error.
195
+ try:
196
+ import camoufox # type: ignore[import-unresolved] # noqa: F401
197
+ except ImportError:
198
+ print(
199
+ json.dumps({
200
+ "jsonrpc": "2.0",
201
+ "id": None,
202
+ "error": {
203
+ "code": -32000,
204
+ "message": (
205
+ "cloverlabs-camoufox is not installed.\n"
206
+ "Run: pip install cloverlabs-camoufox[geoip] && "
207
+ "python -m camoufox fetch"
208
+ ),
209
+ },
210
+ })
211
+ )
212
+ sys.stdout.flush()
213
+ sys.exit(1)
214
+
215
+ bridge = CamoufoxPyBridge()
216
+ bridge.run()