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
package/README.md
CHANGED
|
@@ -1,57 +1,117 @@
|
|
|
1
1
|
# pi-lean-dimension
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
unified under `/web`.
|
|
3
|
+
> Web browsing and search tools for [Pi](https://github.com/earendil-works/pi-coding-agent), the AI coding agent.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
A monorepo housing three Pi extension packages that give your AI agent the
|
|
6
|
+
ability to browse the web interactively, fetch static pages as Markdown, and
|
|
7
|
+
search via SearXNG — all toggled from a single `/web` command. When the toggle
|
|
8
|
+
is off, the tools are removed from the agent's context entirely, so web browsing
|
|
9
|
+
doesn't consume tokens or attention on sessions that aren't doing web work.
|
|
10
|
+
The same surfaces are user-extensible: author navigation guides that resurface
|
|
11
|
+
by domain, or drop in a stealth browser backend like [Camoufox](https://github.com/daijro/camoufox)
|
|
12
|
+
when a site blocks the shipped Chromium/Firefox.
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Quick start (recommended)
|
|
12
15
|
|
|
13
16
|
```bash
|
|
14
|
-
pi install npm:pi-lean-
|
|
17
|
+
pi install npm:pi-lean-portal
|
|
18
|
+
npx playwright install chromium firefox
|
|
15
19
|
```
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
That's it — the tools are registered and start enabled by default. Control
|
|
22
|
+
them with `/web on|off|learn` (`on` = browser tools, `learn` = browser tools
|
|
23
|
+
plus guide-saving via `web-learn`, `off` = everything off). The state persists
|
|
24
|
+
per session. To set a different default for **new** sessions, add this to your
|
|
25
|
+
Pi settings (`~/.pi/agent/settings.json` or `.pi/settings.json`):
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
```json
|
|
28
|
+
{ "browserToggle": { "defaultEnabled": false } }
|
|
29
|
+
```
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Install options
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
| Mode | Command | What you get | Requires |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| **A — Browser** (recommended) | `pi install npm:pi-lean-portal` | 12 browser tools + `/web` command | `npx playwright install chromium firefox` |
|
|
38
|
+
| **B — Full suite** | `pi install npm:pi-lean-dimension` | 13 tools (browser + search) + `/web` | Playwright browsers + SearXNG server |
|
|
39
|
+
| **B-search — Search only** | `pi install npm:pi-lean-search` | `web-search` tool only | SearXNG server |
|
|
40
|
+
|
|
41
|
+
Notes the table doesn't carry:
|
|
42
|
+
|
|
43
|
+
- **Browser binaries aren't downloaded during `npm install`** (configured via `.npmrc`). The first `browser-navigate` call prompts you to run `npx playwright install chromium firefox` if they're missing.
|
|
44
|
+
- **SearXNG is optional for Mode B.** The browser works immediately without it; `web-search` returns a clear setup message on first call. When you do run it, point the suite at your instance in Pi settings:
|
|
26
45
|
|
|
27
46
|
```json
|
|
28
|
-
{ "searxng": { "url": "
|
|
47
|
+
{ "searxng": { "url": "http://localhost:8888" } }
|
|
29
48
|
```
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
50
|
+
- **Search-only has no `/web` command** — a single tiny tool has nothing to toggle.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## What's included
|
|
55
|
+
|
|
56
|
+
### Packages
|
|
57
|
+
|
|
58
|
+
| Package | Type | Description |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `pi-lean-portal` | Extension | Interactive browser + `/web` command owner. **12 tools + 1 command.** |
|
|
61
|
+
| `pi-lean-search` | Extension | SearXNG search tool (`web-search`). **1 tool + 1 command** (`/searxng-status`). |
|
|
62
|
+
| `pi-lean-dimension` | Umbrella meta-package | Bundles portal + search for one-command install. |
|
|
63
|
+
|
|
64
|
+
### Tools (13 total with search)
|
|
65
|
+
|
|
66
|
+
| Tool | Package | Purpose |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `browser-navigate` | portal | Navigate to a URL, get an accessibility tree with `@e` element refs |
|
|
69
|
+
| `browser-snapshot` | portal | Re-extract the current page's accessibility tree (`@e` refs) and capture a screenshot to a temp file |
|
|
70
|
+
| `browser-click` | portal | Click an element by `@e` ref |
|
|
71
|
+
| `browser-type` | portal | Type text into an input by `@e` ref |
|
|
72
|
+
| `browser-scroll` | portal | Scroll the page |
|
|
73
|
+
| `browser-back` | portal | Navigate back |
|
|
74
|
+
| `browser-press` | portal | Press a key |
|
|
75
|
+
| `browser-console` | portal | Read console messages from the page |
|
|
76
|
+
| `browser-inspect` | portal | Query and extract text from elements |
|
|
77
|
+
| `web-fetch` | portal | Stateless fetch → Markdown (no JS) |
|
|
78
|
+
| `web-guide` | portal | Navigation guidance for a site or pattern |
|
|
79
|
+
| `web-learn` | portal | Save/update navigation guidance |
|
|
80
|
+
| `web-search` | search | Web search via SearXNG |
|
|
51
81
|
|
|
52
82
|
### Commands
|
|
53
83
|
|
|
54
|
-
|
|
84
|
+
| Command | Owner | Description |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `/web on\|off\|learn\|cookies\|profile\|status` | portal | Unified toggle and management |
|
|
87
|
+
| `/searxng-status` | search | Test SearXNG connection and update status glyph |
|
|
88
|
+
|
|
89
|
+
### Status bar
|
|
90
|
+
|
|
91
|
+
When search is installed, two independent glyphs appear:
|
|
92
|
+
|
|
93
|
+
- `● idle` (browser) — browser tools enabled
|
|
94
|
+
- `● searxng` (search) — SearXNG health (accent=healthy, yellow=degraded, red=unreachable)
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Extending it
|
|
99
|
+
|
|
100
|
+
Beyond the `/web` toggle, two surfaces are user-driven rather than hardcoded:
|
|
101
|
+
|
|
102
|
+
- **Navigation guides** — `web-learn` saves site-specific playbooks that auto-match by domain and resurface in later sessions.
|
|
103
|
+
- **Custom browser backends** — if a site blocks the shipped Chromium/Firefox, drop a `bridge.py` subclass into `~/.pi/agent/pi-lean-portal/user-backends/` and drive a patched engine like [Camoufox](https://github.com/daijro/camoufox) yourself. A quirks schema declares how the engine diverges from base Playwright, and `launch` options flow from `settings.json` to the subprocess at runtime. This is user-authored, user-audited code that the extension never auto-downloads — and as far as we're aware, no other Pi web plugin lets you run a browser backend you wrote yourself. Most installs never need it; the [portal README](packages/pi-lean-portal/README.md#stealth--custom-browser-backends) and [`contributed/README.md`](packages/pi-lean-portal/contributed/README.md) cover the full flow when you do.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/coreyryanhanson/pi-lean-dimension.git
|
|
111
|
+
cd pi-lean-dimension
|
|
112
|
+
npm install
|
|
113
|
+
npm test # vitest run — all workspace tests
|
|
114
|
+
```
|
|
55
115
|
|
|
56
116
|
## License
|
|
57
117
|
|
|
@@ -32,10 +32,146 @@ getCookies, addCookies, clearCookies — cookie operations
|
|
|
32
32
|
getStorageState — profile storage for session restore
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The 12 registered tools map to 12 tool-facing plugin methods. The cookie/storage methods (`getCookies`, `addCookies`, `clearCookies`, `getStorageState`) are router-facing, not tool-mapped. Element cache access (`getElementCache`) is used internally by `browser-inspect`. The lifecycle methods (`init`, `cleanupAll`) are framework-facing. Total interface: 19 methods.
|
|
35
|
+
The 12 registered tools map to 12 tool-facing plugin methods. The cookie/storage methods (`getCookies`, `addCookies`, `clearCookies`, `getStorageState`) are router-facing, not tool-mapped. Element cache access (`getElementCache`) is used internally by `browser-inspect`. The lifecycle methods (`init`, `cleanupAll`) are framework-facing. Total interface: 19 methods (18 required + 1 optional).
|
|
36
36
|
|
|
37
37
|
Capabilities (`PluginCapabilities`) advertise quirks. The router checks them at dispatch time.
|
|
38
38
|
|
|
39
|
+
## Browser launch hook
|
|
40
|
+
|
|
41
|
+
The portal's own backends launch their browser directly and drive
|
|
42
|
+
their own page — there is **no external-attach path**. The
|
|
43
|
+
`getAttachEndpoint()` interface method, the `AttachEndpoint` union,
|
|
44
|
+
`core/shared/cdp-endpoint.ts`, the `launchServer()` / `connect()`
|
|
45
|
+
hop, and the `_cdpEndpoint` / `_wsEndpoint` / `_browserServer` /
|
|
46
|
+
`_reconnectBrowser()` scaffolding were all removed (see
|
|
47
|
+
[`docs/decisions/miniwob-and-host-setup.md`](../../docs/decisions/miniwob-and-host-setup.md)).
|
|
48
|
+
There is no post-launch hook for third-party subclasses and no
|
|
49
|
+
`connectOverCDP?` interface hook — a host-owns-browser ("Mode B")
|
|
50
|
+
path was considered and dropped as YAGNI; either will be re-added
|
|
51
|
+
alongside a real consumer that needs it.
|
|
52
|
+
|
|
53
|
+
## Stealth backends (user-managed)
|
|
54
|
+
|
|
55
|
+
Stealth backends are **user-installed plugins** that drive
|
|
56
|
+
patched/fingerprint-managed browser binaries (e.g. Camoufox) for sites
|
|
57
|
+
that block the shipped Chromium/Firefox. They are never shipped in the
|
|
58
|
+
npm tarball and the extension never downloads or executes them
|
|
59
|
+
automatically — there is no plugin marketplace. For the install flow
|
|
60
|
+
and the choosing decision, see
|
|
61
|
+
[`contributed/README.md`](contributed/README.md) and
|
|
62
|
+
[`contributed/CHOOSING.md`](contributed/CHOOSING.md).
|
|
63
|
+
|
|
64
|
+
### Deployment: user-writable data tree, not the npm tarball
|
|
65
|
+
|
|
66
|
+
Stealth backends live under the user-writable data tree:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
~/.pi/agent/pi-lean-portal/
|
|
70
|
+
├── web-guides/ (existing)
|
|
71
|
+
├── browser-state/ (existing)
|
|
72
|
+
└── user-backends/ ← stealth backends go here
|
|
73
|
+
└── camoufox-py/
|
|
74
|
+
├── bridge.py (user copies from contributed/)
|
|
75
|
+
└── .venv/ (user-created: engine pip pkg + playwright)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This is **trusted user code** — the user wrote or audited the bridge,
|
|
79
|
+
created the venv, and fetched the binary. The extension spawns it as a
|
|
80
|
+
subprocess; it never auto-downloads stealth backends. `user-backends/`
|
|
81
|
+
is a **different tree from any in-repo test fixtures** (which live
|
|
82
|
+
gitignored under `bench/miniwob/fixtures/` for the evaluation
|
|
83
|
+
harness). The `~/.pi/agent/pi-lean-portal/user-backends/` tree is what
|
|
84
|
+
the pi agent's production `detectPluginType` reads at runtime.
|
|
85
|
+
|
|
86
|
+
Stealth backends are **never in the default fallback list**. When
|
|
87
|
+
`browser.plugins` is absent, only the four shipped backends
|
|
88
|
+
(`chromium`, `firefox`, `chromium-py`, `firefox-py`) are loaded. A
|
|
89
|
+
fresh install must not emit validation errors for plugins the user
|
|
90
|
+
never asked for. Tested in `__tests__/plugin-config-browser.test.ts`.
|
|
91
|
+
|
|
92
|
+
### Discovery: multi-root, absolute short-circuit
|
|
93
|
+
|
|
94
|
+
`detectPluginType(dir, roots)` (in `core/plugin-config.ts`) resolves
|
|
95
|
+
`dir` in order:
|
|
96
|
+
|
|
97
|
+
1. **Absolute path** — used directly (dev/power-user escape hatch).
|
|
98
|
+
2. **`DEFAULT_BACKEND_ROOTS[0]`** = package `backends/` (shipped
|
|
99
|
+
backends).
|
|
100
|
+
3. **`DEFAULT_BACKEND_ROOTS[1]`** = `USER_BACKENDS_DIR` (user stealth).
|
|
101
|
+
|
|
102
|
+
First root with an unambiguous entry point (`index.ts` XOR `bridge.py`)
|
|
103
|
+
wins. Missing from all roots throws an error naming every root
|
|
104
|
+
searched. Tested in `__tests__/plugin-loading.test.ts`.
|
|
105
|
+
|
|
106
|
+
The `pythonPath` in `config` must be **absolute** — a relative
|
|
107
|
+
`pythonPath` is not resolved against `USER_BACKENDS_DIR` (that nicety
|
|
108
|
+
is intentionally out of scope). Point it at
|
|
109
|
+
`<user-backends>/<name>-py/.venv/bin/python`.
|
|
110
|
+
|
|
111
|
+
### Config channel: `browser.init` RPC
|
|
112
|
+
|
|
113
|
+
After the `ping` handshake, `python-adapter.ts` sends a single
|
|
114
|
+
`browser.init` RPC with `{ config: <user config dict> }`. The bridge
|
|
115
|
+
stores it as `self._plugin_config`; subclasses read
|
|
116
|
+
`self.plugin_config.get("launch", {})`. A bridge that does not
|
|
117
|
+
recognize `browser.init` rejects with a "bridge too old" message so
|
|
118
|
+
upgrades fail loudly. Re-sent after crash-recovery restarts. Tested in
|
|
119
|
+
`__tests__/python-adapter.test.ts` ("browser.init RPC").
|
|
120
|
+
|
|
121
|
+
### Importability: `PYTHONPATH` injection
|
|
122
|
+
|
|
123
|
+
`python-adapter.ts` `_buildPythonPath()` appends the package's
|
|
124
|
+
`backends/python-base/` to any existing `PYTHONPATH` in the spawn env,
|
|
125
|
+
so a user bridge in its own venv can
|
|
126
|
+
`from pi_browser_bridge.playwright_base import PlaywrightBridge`
|
|
127
|
+
without a `pip install` of `pi-browser-bridge` (which is not on PyPI).
|
|
128
|
+
Append (not prepend) so the editable install in `python-base/.venv`
|
|
129
|
+
keeps precedence for the shipped `chromium-py` / `firefox-py` bridges.
|
|
130
|
+
Tested in `__tests__/python-adapter.test.ts` ("PYTHONPATH injection").
|
|
131
|
+
|
|
132
|
+
### Quirks schema (`PlaywrightBridge` class attrs)
|
|
133
|
+
|
|
134
|
+
The contract for a stealth backend is a set of class attributes on
|
|
135
|
+
`PlaywrightBridge` in
|
|
136
|
+
`backends/python-base/pi_browser_bridge/playwright_base.py`. Set them
|
|
137
|
+
as class attributes on your subclass:
|
|
138
|
+
|
|
139
|
+
| Flag | Default | Effect when set |
|
|
140
|
+
|------|---------|-----------------|
|
|
141
|
+
| `_fingerprint_managed_context` | `False` | `create_browser_context()` skips hardcoded `viewport`/`user_agent`; lets the fingerprint package set them. |
|
|
142
|
+
| `_eval_prefix` | `""` | Prepended to every `page.evaluate` expression in `do_evaluate` (e.g. Camoufox's `"mw:"` routes writes to the main world). |
|
|
143
|
+
| `_scroll_via_wheel` | `False` | `do_scroll` uses `page.mouse.wheel` instead of `page.evaluate("window.scrollBy")` (avoids eval-write under isolated-world stealth). |
|
|
144
|
+
| `_skip_default_viewport` | `False` | Skips Playwright's `Browser.setDefaultViewport` CDP call (Camoufox binary rejects its `isMobile` prop). |
|
|
145
|
+
| `_skip_networkidle` | `False` | Nav-settle uses `load` instead of `networkidle` (patched binaries don't fire `networkidle` reliably). |
|
|
146
|
+
| `_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 (see prose below). |
|
|
147
|
+
| `_csp_safe_readonly_via_init_script` | `False` | `do_evaluate(read_only=True)` (the EXTRACTOR_SCRIPT) reads its JSON result from a `<meta id="__pi-extract">` tag that `create_browser_context` registers as a `context.add_init_script` (isolated world, CSP-free) at `DOMContentLoaded`, instead of `page.evaluate`. For patched-Firefox stealth binaries that route `page.evaluate` through `eval()` in the page's main world (CSP-subject) — Camoufox is NOT affected (its binary keeps Juggler's CSP-free isolated-world). The adapter plumbs the script via the `browser.init` config key `readOnlyExtractorScript`. Stale across SPA route changes (no new load) — fine for navigate→inspect. |
|
|
148
|
+
|
|
149
|
+
All flags default off → `chromium-py` / `firefox-py` behavior is
|
|
150
|
+
bit-identical to a pre-stealth install. A dropped v2 `_context_factory`
|
|
151
|
+
flag (dispatching to a `_camoufox_new_context` helper) was removed
|
|
152
|
+
when `camoufox.NewContext` turned out to be broken on the current
|
|
153
|
+
binary (`Protocol error (Browser.setDefaultViewport)` from the same
|
|
154
|
+
`isMobile` rejection `_skip_default_viewport` handles). Camoufox
|
|
155
|
+
injects the fingerprint at **browser launch** via `camoufox.NewBrowser`,
|
|
156
|
+
so standard `browser.new_context()` with
|
|
157
|
+
`_fingerprint_managed_context = True` is correct — do not re-attempt
|
|
158
|
+
`NewContext`.
|
|
159
|
+
|
|
160
|
+
### Camoufox: the shipped example template
|
|
161
|
+
|
|
162
|
+
Camoufox is the **shipped, tested template** — a reference `bridge.py`
|
|
163
|
+
lives at `contributed/camoufox-py/` (source repo only; **not in the
|
|
164
|
+
npm tarball** because `docs/` is excluded from `package.json` `files`).
|
|
165
|
+
Pointer:
|
|
166
|
+
`packages/pi-lean-portal/contributed/camoufox-py/bridge.py`.
|
|
167
|
+
Generic test suites (contract + persistence + MiniWoB parity + quirks
|
|
168
|
+
introspection) run via the discovery runner at
|
|
169
|
+
`__tests__/run-contributed-suites.test.ts`, which auto-discovers
|
|
170
|
+
any `<name>-py/` user backend at runtime — there is no per-backend
|
|
171
|
+
contract file for Camoufox. Backend-specific behavioural tests
|
|
172
|
+
(beyond the quirks flags) are optional hand-authored files under
|
|
173
|
+
`__tests__/contributed/<name>-py/`
|
|
174
|
+
|
|
39
175
|
## Router (`core/router.ts`)
|
|
40
176
|
|
|
41
177
|
All tool calls dispatch through the router. Key responsibilities:
|
|
@@ -68,12 +204,12 @@ All tool calls dispatch through the router. Key responsibilities:
|
|
|
68
204
|
|
|
69
205
|
## Known Constraints & Debt
|
|
70
206
|
|
|
71
|
-
- **Console capture in Python backends** — Both `chromium-py` and `firefox-py` inherit console capture (500-entry ring buffer) and dialog auto-dismissal from `PlaywrightBridge._setup_page_session()` in `python-base`. The base `BrowserBridge` does not
|
|
207
|
+
- **Console capture in Python backends** — Both `chromium-py` and `firefox-py` inherit console capture (500-entry ring buffer) and dialog auto-dismissal from `PlaywrightBridge._setup_page_session()` in `python-base`. The base `BrowserBridge` (an `abc.ABC`) does not define `_setup_page_session`; future Python plugins that subclass `BrowserBridge` directly must supply their own session setup (subclasses of `PlaywrightBridge` inherit it).
|
|
72
208
|
- **AbortSignal not supported on Python bridge** — the router passes `signal` through unconditionally (no capability check). The Python adapter accepts and silently ignores the signal. `supportsAbortSignal` is advertised but unenforced.
|
|
73
209
|
- **Sessions are per taskId** — mapped to `browser-NNN` keys via `_sessionKeys`/`_sessionCounter` in `core/shared/task-id.ts`. Created on first navigate, cleaned up on `session_shutdown`.
|
|
74
210
|
- **Python shared-context machinery removed (B1)** — the `browser.newPage`/`browser.closePage` RPC routes, `_profile_contexts` ref-counting, and `ensure_profile_session`/`remove_profile_session` methods were removed from both the base `BrowserBridge` and `ChromiumPyBridge`. Named profiles now use disk persistence (load-on-navigate via `storageState`) matching the TS Chromium plugin. Both backends use `ensure_session(task_id, config)` for all sessions.
|
|
75
211
|
- **Python bridge reuses BrowserContexts across navigations** — `ensure_session()` returns the existing session on re-navigate (unlike the TS Chromium plugin which creates a fresh context per navigate). This means in-process cookies survive re-navigation without explicit save, but also means `storageState` from the router is ignored on re-navigate (the context already exists). The Python adapter's `_persistState()` saves current cookies to disk before the navigate RPC for cross-process persistence.
|
|
76
|
-
- **`_persistState()` helper in both backends** —
|
|
212
|
+
- **`_persistState()` helper in both backends** — both `PlaywrightPluginBase._persistState` (direct `context.storageState()` call) and `PythonPluginAdapter._persistState` (JSON-RPC `browser.getStorageState` retrieval) delegate to the shared `persistSessionState()` helper in `core/shared/storage-state.ts`, which owns the `session?.persistState` gate, the `saveStorageState()` call, and the warn-and-swallow error path. Called both from `cleanup()` and — on re-navigate — from `getOrCreateContext()` (Chromium) or `navigate()` (Python) before the old context is closed/reused.
|
|
77
213
|
- **Role-based locators only**: never XPath/CSS — always `getByRole()` via `buildLocator()` with positional `.nth()` for duplicates. The `INTERACTIVE_ROLES` set defines which roles get @e refs.
|
|
78
214
|
- **All URLs go through `url-safety.ts`** — blocks localhost, private IPs (10.x, 172.16-31.x, 192.168.x, 169.254.169.254), dangerous schemes (file:, ftp:, data:, javascript:, vbscript:), and heuristically detects secrets in URLs.
|
|
79
215
|
- **Screenshot**: JPEG 80% quality, viewport constrained to 1280px wide, returns data URI.
|
|
@@ -87,10 +223,16 @@ All tool calls dispatch through the router. Key responsibilities:
|
|
|
87
223
|
- **Guide staleness**: no builtin site guides shipped — entirely user-authored via `~/.pi/agent/pi-lean-portal/web-guides/*.md`. Guides carry `updated` date and `currentDate` timestamp in output.
|
|
88
224
|
- **Learn mode toggle**: `/web learn` enables `web-learn` tool; `/web on` removes it. Agent never calls `web-learn` unprompted. Default is off on fresh sessions.
|
|
89
225
|
- **Navigation settle** (`core/shared/nav-settle.ts`): after click or press, detects page navigation via a `framenavigated` listener and waits for `load + networkidle` (capped, errors swallowed) before reading URL/title/snapshot. Replaces the old fixed `waitForTimeout(300)` pattern that caused URL/DOM mismatches. Framework-agnostic via a lightweight `NavigationSettlePage` interface for testability.
|
|
226
|
+
- **Stealth backends are user-managed, not shipped** — they live under `~/.pi/agent/pi-lean-portal/user-backends/`, are never in the npm tarball, and the extension never auto-downloads them. The user-side install burden is real: a per-engine venv, a ~100 MB patched-binary fetch, and an explicit `settings.json` entry with an **absolute** `pythonPath`. See `contributed/README.md` for the install flow.
|
|
227
|
+
- **Fingerprint-managed context** — a stealth backend sets `_fingerprint_managed_context = True` so `create_browser_context()` skips the hardcoded `viewport`/`user_agent` and lets the fingerprint package set them. Camoufox injects the fingerprint at **browser launch** via `camoufox.NewBrowser`, so standard `browser.new_context()` is correct (a `_context_factory` / `NewContext` path was attempted and dropped — `camoufox.NewContext` is broken on the current binary).
|
|
228
|
+
- **Camoufox `mw:` prefix + `main_world_eval`** — Camoufox sets `_eval_prefix = "mw:"` so `do_evaluate` writes route to the main world (isolated-world stealth otherwise blocks them), and forwards `main_world_eval=True` to `NewBrowser`. Contract tests assert `do_evaluate("() => 1 + 1")` returns `2`.
|
|
229
|
+
- **`isMobile` / `_skip_default_viewport` binary quirk** — Camoufox's patched Firefox binary rejects the `isMobile` prop in Playwright's `Browser.setDefaultViewport` CDP call, so the template sets `_skip_default_viewport = True`. If a future binary version fixes the rejection, the flag degrades gracefully (default off) — re-validate on Camoufox releases.
|
|
230
|
+
- **`_wrap_mw_eval_in_eval` main-world statement support** — Camoufox's patched Juggler main-world eval path (`MainWorldContext.executeInGlobal` in the binary's `omni.ja`) wraps every `mw:`-prefixed script as `(() => { let _s = (${script}); ... })()`. That wrapper requires `${script}` to be a single *expression*; any *statement* — `let` / `var` / multiple `;`-separated statements (the exact shape of the MiniWoB setup scripts `REMOVE_DISPLAY_JS` / `SETUP_JS`) — is a `SyntaxError` (`missing ) in parenthetical`) that surfaces through Playwright as `"Execution context was destroyed, most likely because of a navigation."`. That error is **not** a navigation race (the previous `_retry_eval_on_context_destroyed` quirk retried it and could never succeed — a SyntaxError is deterministic). The template sets `_wrap_mw_eval_in_eval = True`, making `do_evaluate` rewrite the script as `mw:eval(<JSON-string of script>)`: a single expression (valid inside `let _s = (...)`) where `eval` runs the script verbatim and returns its completion value, handling both expressions and multi-statement scripts. When a future Camoufox driver release fixes the wrapper, flip the flag back to `False`.
|
|
231
|
+
- **`xvfb` for `headless='virtual'`** — on Linux, Camoufox's `headless='virtual'` mode needs the `xvfb` system package; true headless (`headless=True`, the template's default) works without it.
|
|
90
232
|
- **`BROWSER_DEBUG=1`** — enables structured `[browser]` log lines on stderr (navigate, snapshot, click). Checked in both ChromiumPlugin and the Python bridge.
|
|
91
233
|
|
|
92
234
|
## Debugging
|
|
93
235
|
|
|
94
236
|
```bash
|
|
95
|
-
BROWSER_DEBUG=1 npx vitest run __tests__/
|
|
237
|
+
BROWSER_DEBUG=1 npx vitest run __tests__/chromium.test.ts
|
|
96
238
|
```
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# pi-lean-portal User Guide
|
|
2
2
|
|
|
3
|
-
> **pi-lean-portal**
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
3
|
+
> **pi-lean-portal** gives the Pi coding agent interactive web browsing —
|
|
4
|
+
> Playwright Chromium/Firefox, accessibility-tree snapshots with `@e` element
|
|
5
|
+
> refs, persistent profiles, cookies, and navigation guides that resurface by
|
|
6
|
+
> domain. A `/web` toggle removes the tools from the agent's context when
|
|
7
|
+
> switched off, so web browsing doesn't consume tokens on sessions that aren't
|
|
8
|
+
> doing web work. If a site blocks the shipped browsers, drop in your own
|
|
9
|
+
> backend (e.g. [Camoufox](https://github.com/daijro/camoufox)) - as far as
|
|
10
|
+
> we're aware, no other Pi web plugin lets you run a browser backend you wrote
|
|
11
|
+
> yourself.
|
|
8
12
|
>
|
|
9
13
|
> Part of the [pi-lean-dimension](https://github.com/coreyryanhanson/pi-lean-dimension)
|
|
10
14
|
> web-tools suite. For SearXNG search support, install
|
|
@@ -15,16 +19,17 @@
|
|
|
15
19
|
## Table of Contents
|
|
16
20
|
|
|
17
21
|
1. [Quick Start](#quick-start)
|
|
18
|
-
2. [
|
|
19
|
-
3. [
|
|
20
|
-
4. [
|
|
21
|
-
5. [
|
|
22
|
-
6. [
|
|
23
|
-
7. [
|
|
24
|
-
8. [
|
|
25
|
-
9. [
|
|
26
|
-
10. [
|
|
27
|
-
11. [
|
|
22
|
+
2. [Extending it](#extending-it)
|
|
23
|
+
3. [`/web` Command — Browser Toggle & Profiles](#web-command--browser-toggle--profiles)
|
|
24
|
+
4. [All 12 Tools](#all-12-tools)
|
|
25
|
+
5. [Stateless Fetching (web-fetch)](#stateless-fetching-web-fetch)
|
|
26
|
+
6. [Navigation Guides (web-guide & web-learn)](#navigation-guides-web-guide--web-learn)
|
|
27
|
+
7. [`/web status` — Detailed Runtime Status](#web-status--detailed-runtime-status)
|
|
28
|
+
8. [Profiles — Persistent Sessions](#profiles--persistent-sessions)
|
|
29
|
+
9. [Cookie Management](#cookie-management)
|
|
30
|
+
10. [Backend Architecture](#backend-architecture)
|
|
31
|
+
11. [Configuration (settings.json)](#configuration-settingsjson)
|
|
32
|
+
12. [Tips & Best Practices](#tips--best-practices)
|
|
28
33
|
|
|
29
34
|
---
|
|
30
35
|
|
|
@@ -56,6 +61,21 @@ The browser tools are **enabled by default**. You can:
|
|
|
56
61
|
|
|
57
62
|
---
|
|
58
63
|
|
|
64
|
+
## Extending it
|
|
65
|
+
|
|
66
|
+
Beyond the toggle, two surfaces are user-extensible rather than hardcoded:
|
|
67
|
+
|
|
68
|
+
- **Navigation guides** — `web-learn` saves site-specific playbooks that
|
|
69
|
+
auto-match by domain and resurface in later sessions. See
|
|
70
|
+
[Navigation Guides](#navigation-guides-web-guide--web-learn).
|
|
71
|
+
- **Custom browser backends** — if a site blocks the shipped Chromium/Firefox,
|
|
72
|
+
drop a `bridge.py` subclass into `~/.pi/agent/pi-lean-portal/user-backends/`
|
|
73
|
+
and drive a patched engine like [Camoufox](https://github.com/daijro/camoufox)
|
|
74
|
+
yourself. The full flow lives in [Backend Architecture](#backend-architecture)
|
|
75
|
+
and [`contributed/README.md`](./contributed/README.md).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
59
79
|
## `/web` Command — Browser Toggle & Profiles
|
|
60
80
|
|
|
61
81
|
The `/web` command controls whether web tools are visible to the AI agent,
|
|
@@ -414,39 +434,76 @@ capability advertisement:
|
|
|
414
434
|
- The extension auto-detects whether a plugin is Node-based (`index.ts`)
|
|
415
435
|
or Python-based (`bridge.py`) by inspecting the directory.
|
|
416
436
|
|
|
417
|
-
### Stealth & Custom Browser Backends
|
|
437
|
+
### Stealth & Custom Browser Backends
|
|
418
438
|
|
|
419
439
|
This package ships four backends — `chromium`, `firefox`, `chromium-py`,
|
|
420
440
|
and `firefox-py` — all built on Playwright. Additional browser support
|
|
421
441
|
(including stealth engines like **Camoufox**) is intentionally **left
|
|
422
442
|
to users** to author and drop in, rather than being bundled with the
|
|
423
|
-
package.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
443
|
+
package. The infrastructure for this is now in place: a **quirks system**
|
|
444
|
+
lets a backend declare how it diverges from the base Playwright behavior,
|
|
445
|
+
and a **config channel** (`browser.init` RPC) forwards launch options
|
|
446
|
+
from `settings.json` to the Python bridge subprocess.
|
|
447
|
+
|
|
448
|
+
Most users will never need a stealth backend (see
|
|
449
|
+
[`contributed/CHOOSING.md`](./contributed/CHOOSING.md) for when to reach
|
|
450
|
+
for one at all). When you do, the flow is:
|
|
451
|
+
|
|
452
|
+
1. **Drop a `bridge.py` into the user-backends tree.** The convention is
|
|
453
|
+
`~/.pi/agent/pi-lean-portal/user-backends/<name>-py/bridge.py` (the
|
|
454
|
+
`-py` suffix mirrors the shipped `chromium-py` / `firefox-py`). This is
|
|
455
|
+
a separate tree from the package's own `backends/` directory, which is
|
|
456
|
+
not edited after install — exactly so custom backends survive updates.
|
|
457
|
+
2. **Create a venv and fetch the engine binary** (e.g.
|
|
458
|
+
`python -m camoufox fetch`). The shared `pi_browser_bridge` library is
|
|
459
|
+
injected onto `PYTHONPATH` automatically at spawn time, so you do not
|
|
460
|
+
need to `pip install` it.
|
|
461
|
+
3. **Register it in `browser.plugins`** with an **absolute** `pythonPath`
|
|
462
|
+
and a `launch` object whose keys are forwarded to the bridge.
|
|
463
|
+
4. **Verify with `/web status`** and a `browser-navigate`.
|
|
464
|
+
|
|
465
|
+
The shipped **Camoufox template** at
|
|
466
|
+
[`contributed/camoufox-py/bridge.py`](./contributed/camoufox-py/bridge.py)
|
|
467
|
+
is a worked example — copy it as a starting point. The full install flow,
|
|
468
|
+
the quirks schema reference, and the security model (user-backends are
|
|
469
|
+
**trusted user code** — never auto-downloaded, no plugin marketplace) live
|
|
470
|
+
in [`contributed/README.md`](./contributed/README.md). The decision doc at
|
|
471
|
+
[`contributed/CHOOSING.md`](./contributed/CHOOSING.md) covers when to use
|
|
472
|
+
a stealth backend at all and the two lifecycle patterns for implementing
|
|
473
|
+
your own.
|
|
474
|
+
|
|
475
|
+
#### Writing your own backend (high level)
|
|
476
|
+
|
|
477
|
+
A custom Python backend is a subclass of `PlaywrightBridge`
|
|
478
|
+
(`backends/python-base/pi_browser_bridge/playwright_base.py`) that sets
|
|
479
|
+
the **quirks flags** its engine needs as class attributes and overrides
|
|
480
|
+
the launch hook matching how the engine owns Playwright. The flags
|
|
481
|
+
(`_fingerprint_managed_context`, `_eval_prefix`, `_scroll_via_wheel`,
|
|
482
|
+
`_skip_default_viewport`, `_skip_networkidle`, `_wrap_mw_eval_in_eval`)
|
|
483
|
+
all default off, so a subclass that sets none of them is bit-identical to
|
|
484
|
+
the shipped `chromium-py` / `firefox-py`. The full table with effects is
|
|
485
|
+
in [`contributed/README.md`](./contributed/README.md#quirks-schema-reference).
|
|
486
|
+
|
|
487
|
+
Node-based custom backends follow the same shape via the
|
|
488
|
+
`PlaywrightPluginBase` class — the auto-detection in `plugin-loading`
|
|
489
|
+
picks up `index.ts` (Node) or `bridge.py` (Python) entry points from the
|
|
490
|
+
user-backends directory.
|
|
491
|
+
|
|
492
|
+
#### Tests are auto-discovered
|
|
493
|
+
|
|
494
|
+
You usually do **not** need to write your own tests. The contributed
|
|
495
|
+
runner at `__tests__/run-contributed-suites.test.ts` discovers every
|
|
496
|
+
backend under `user-backends/*-py/`, loads config from the test-local
|
|
497
|
+
`settings.json`, and runs the shared contract + persistence + parity +
|
|
498
|
+
quirks-introspection suites against it — forwarding your configured
|
|
499
|
+
`launch` options. Opt in with `CONTRIB_RUN=1`:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
npm run setup:miniwob # one-time: clone MiniWoB++ content
|
|
503
|
+
CONTRIB_RUN=1 npx vitest run packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
A custom backend's config entry looks like:
|
|
450
507
|
|
|
451
508
|
```jsonc
|
|
452
509
|
{
|
|
@@ -454,8 +511,9 @@ The shape a custom backend's config entry will take looks like:
|
|
|
454
511
|
"plugins": [
|
|
455
512
|
{ "name": "chromium", "dir": "chromium", "enabled": true, "config": {} },
|
|
456
513
|
{ "name": "firefox", "dir": "firefox", "enabled": true, "config": {} },
|
|
457
|
-
{ "name": "camoufox-py", "dir": "camoufox-py", "enabled":
|
|
458
|
-
"pythonPath": "/
|
|
514
|
+
{ "name": "camoufox-py", "dir": "camoufox-py", "enabled": true, "config": {
|
|
515
|
+
"pythonPath": "/home/me/.pi/agent/pi-lean-portal/user-backends/camoufox-py/.venv/bin/python",
|
|
516
|
+
"launch": { "headless": true, "os": "windows", "humanize": true }
|
|
459
517
|
}
|
|
460
518
|
}
|
|
461
519
|
]
|
|
@@ -463,8 +521,12 @@ The shape a custom backend's config entry will take looks like:
|
|
|
463
521
|
}
|
|
464
522
|
```
|
|
465
523
|
|
|
466
|
-
|
|
467
|
-
|
|
524
|
+
`pythonPath` must be **absolute**; `dir` resolves against the user-backends
|
|
525
|
+
root (multi-root discovery: package `backends/` → `USER_BACKENDS_DIR` →
|
|
526
|
+
absolute). `launch` keys are forwarded to the bridge as
|
|
527
|
+
`plugin_config.launch` via the `browser.init` RPC. Stealth backends are
|
|
528
|
+
never in the default fallback list — a fresh install with no
|
|
529
|
+
`browser.plugins` loads only the four shipped backends.
|
|
468
530
|
|
|
469
531
|
---
|
|
470
532
|
|
|
@@ -495,9 +557,9 @@ Controls which browser backends are loaded. Entries are processed in order
|
|
|
495
557
|
|
|
496
558
|
Each entry requires only a unique name, a backend directory path, and an
|
|
497
559
|
optional `config` object passed to the plugin's `init()`. For the Python
|
|
498
|
-
backends, `config` carries
|
|
499
|
-
`camoufox-py`
|
|
500
|
-
|
|
560
|
+
backends, `config` carries `pythonPath` and a `launch` object (the shape
|
|
561
|
+
shown for `camoufox-py` in the [Stealth & Custom Browser Backends](#stealth--custom-browser-backends)
|
|
562
|
+
section above is the reference for a user-authored Python backend).
|
|
501
563
|
|
|
502
564
|
### `browser.defaultProfile`
|
|
503
565
|
|