pi-lean-dimension 0.1.0 → 0.2.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.
- package/README.md +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- 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
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- 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
- 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
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- 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
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- 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/daijro/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()
|