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
package/README.md CHANGED
@@ -1,57 +1,117 @@
1
1
  # pi-lean-dimension
2
2
 
3
- The full web-tools suite for Pi — interactive web browsing and SearXNG search,
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
- **This is an umbrella meta-package.** It bundles
7
- [`pi-lean-portal`](https://www.npmjs.com/package/pi-lean-portal) (browser) and
8
- [`pi-lean-search`](https://www.npmjs.com/package/pi-lean-search) (SearXNG search)
9
- into a single `pi install` command.
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/nichochar/camoufox)
12
+ when a site blocks the shipped Chromium/Firefox.
10
13
 
11
- ## Install
14
+ ## Quick start (recommended)
12
15
 
13
16
  ```bash
14
- pi install npm:pi-lean-dimension
17
+ pi install npm:pi-lean-portal
18
+ npx playwright install chromium firefox
15
19
  ```
16
20
 
17
- ### Prerequisites
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
- - **Playwright browsers** (for browsing):
27
+ ```json
28
+ { "browserToggle": { "defaultEnabled": false } }
29
+ ```
20
30
 
21
- ```bash
22
- npx playwright install chromium firefox
23
- ```
31
+ ---
32
+
33
+ ## Install options
24
34
 
25
- - **SearXNG instance** (for search, optional): configure the URL in settings:
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": "https://searxng.example.com" } }
47
+ { "searxng": { "url": "http://localhost:8888" } }
29
48
  ```
30
49
 
31
- The browser works immediately after `npx playwright install`. The search tool
32
- self-documents its setup on first call if SearXNG isn't configured.
33
-
34
- ## What you get
35
-
36
- | Tool | Purpose |
37
- |---|---|
38
- | `browser-navigate` | Navigate to a URL, get an accessibility tree with `@e` element refs |
39
- | `browser-snapshot` | Re-extract the current page's accessibility tree (`@e` refs) and capture a screenshot to a temp file |
40
- | `browser-click` | Click an element by `@e` ref |
41
- | `browser-type` | Type text into an input by `@e` ref |
42
- | `browser-scroll` | Scroll the page |
43
- | `browser-back` | Navigate back |
44
- | `browser-press` | Press a key |
45
- | `browser-console` | Read console messages from the page |
46
- | `browser-inspect` | Query and extract text from elements |
47
- | `web-fetch` | Stateless fetch → Markdown (no JS) |
48
- | `web-guide` | Navigation guidance for a site or pattern |
49
- | `web-learn` | Save/update navigation guidance |
50
- | `web-search` | Web search via SearXNG |
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
- - `/web on|off|learn|cookies|profile|status` — unified toggle and management
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/nichochar/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 install handlers; future Python plugins must override `_setup_page_session`.
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** — extracted from `cleanup()`, this method checks `session?.persistState`, snapshots the BrowserContext's storage state, persists it to disk, and returns the raw state for optional in-memory reuse (Chromium uses the return as fallback for the new context; Python returns it for API consistency). Called both from `cleanup()` and — on re-navigate — from `getOrCreateContext()` (Chromium) or `navigate()` (Python) before the old context is closed/reused.
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__/reddit-dialog.test.ts
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** is a plugin-based web browsing extension for the Pi coding
4
- > agent. It gives the AI agent the ability to fetch web pages, interact with
5
- > dynamic sites, inspect page structure, take screenshots, run JavaScript,
6
- > and save/recall navigation guides — all through a set of tools and the `/web`
7
- > command.
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/nichochar/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. [`/web` Command — Browser Toggle & Profiles](#web-command--browser-toggle--profiles)
19
- 3. [All 12 Tools](#all-12-tools)
20
- 4. [Stateless Fetching (web-fetch)](#stateless-fetching-web-fetch)
21
- 5. [Navigation Guides (web-guide & web-learn)](#navigation-guides-web-guide--web-learn)
22
- 6. [`/web status` — Detailed Runtime Status](#web-status--detailed-runtime-status)
23
- 7. [Profiles — Persistent Sessions](#profiles--persistent-sessions)
24
- 8. [Cookie Management](#cookie-management)
25
- 9. [Backend Architecture](#backend-architecture)
26
- 10. [Configuration (settings.json)](#configuration-settingsjson)
27
- 11. [Tips & Best Practices](#tips--best-practices)
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/nichochar/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 (Planned)
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
- Most of the building blocks are already in place: the `BrowserPlugin`
426
- interface, the Python bridge base class (`PlaywrightBridge`), and
427
- config-driven plugin loading that auto-detects `index.ts` (Node) or
428
- `bridge.py` (Python) entry points. Two pieces of infrastructure are still
429
- needed before user-authored stealth backends are practical:
430
-
431
- 1. **A quirks system** — so a backend can declare things like a custom
432
- context factory (e.g. Camoufox's `NewContext` for fingerprint
433
- injection), an eval-script prefix, or a fingerprint-managed viewport,
434
- instead of being clobbered by the base class's hardcoded defaults.
435
- 2. **A config channel** from the TypeScript adapter to the Python bridge
436
- subprocess — so launch options like `headless`, target OS, proxy, and
437
- binary path can reach the bridge.
438
-
439
- Once those land, the plan is for users to author additional backends the
440
- same way they author site guides today — by dropping files into a
441
- user-owned directory (e.g. `~/.pi/agent/pi-lean-portal/backends/`,
442
- analogous to the `web-guides/` directory) and registering them in
443
- `browser.plugins`. The shipped backends live inside the package's own
444
- `backends/` directory; that directory should not be edited after install
445
- (modifications would be lost on the next package update), which is exactly
446
- why a separate user-owned directory is the supported path for custom and
447
- stealth backends.
448
-
449
- The shape a custom backend's config entry will take looks like:
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": false, "config": {
458
- "pythonPath": "/path/to/camoufox-py/.venv/bin/python"
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
- This support will arrive in a future update. Until then, the four shipped
467
- backends can be toggled via the `enabled` field below.
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 options like `pythonPath` (the shape shown for
499
- `camoufox-py` above is representative of how a user-authored Python
500
- backend will be configured).
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