pi-lean-portal 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 (42) hide show
  1. package/README.md +112 -50
  2. package/backends/chromium/index.ts +5 -13
  3. package/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  4. package/backends/chromium-py/bridge.py +0 -2
  5. package/backends/firefox/index.ts +6 -5
  6. package/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  7. package/backends/firefox-py/bridge.py +7 -6
  8. package/backends/playwright-base/playwright-plugin.ts +241 -398
  9. package/backends/python-adapter.ts +182 -83
  10. package/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  11. package/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  12. package/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  13. package/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  14. package/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  15. package/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  16. package/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  17. package/browser-toggle.ts +33 -69
  18. package/core/fetch-backend.ts +0 -5
  19. package/core/plugin-api.ts +6 -33
  20. package/core/plugin-config.ts +75 -58
  21. package/core/plugin-registry.ts +11 -49
  22. package/core/router.ts +59 -98
  23. package/core/shared/accessibility-tree.ts +10 -143
  24. package/core/shared/bot-detection.ts +31 -77
  25. package/core/shared/browser-data.json +183 -0
  26. package/core/shared/browser-data.ts +50 -0
  27. package/core/shared/browser-events.ts +6 -6
  28. package/core/shared/dom-extractor.ts +97 -36
  29. package/core/shared/nav-settle.ts +12 -15
  30. package/core/shared/paths.ts +3 -0
  31. package/core/shared/session-manager.ts +7 -21
  32. package/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  33. package/core/shared/snapshot-cache.ts +7 -6
  34. package/core/shared/storage-state.ts +40 -9
  35. package/index.ts +42 -7
  36. package/package.json +8 -3
  37. package/ship-manifest.test.ts +8 -3
  38. package/tools/browser-inspect.ts +2 -6
  39. package/tools/browser-navigate.ts +7 -5
  40. package/tools/browser-snapshot.ts +2 -5
  41. package/tools/utils.ts +22 -4
  42. package/tools/web-fetch.ts +3 -4
package/README.md CHANGED
@@ -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
 
@@ -3,27 +3,19 @@
3
3
  *
4
4
  * Thin subclass of PlaywrightPluginBase. All shared logic lives in
5
5
  * backends/playwright-base/playwright-plugin.ts.
6
+ *
7
+ * Launches Chromium directly and drives its own page; the plugin is the
8
+ * sole browser owner. No external-attach endpoint is exposed.
6
9
  */
7
10
 
8
11
  import { chromium } from "playwright";
9
12
  import type { Browser } from "playwright";
10
13
  import { PlaywrightPluginBase } from "../playwright-base/playwright-plugin.js";
11
- import {
12
- DEFAULT_CAPABILITIES,
13
- type PluginCapabilities,
14
- } from "../../core/plugin-api.js";
15
-
16
- // ─── Capabilities ──────────────────────────────────────────────────
17
-
18
- const CHROMIUM_CAPABILITIES: PluginCapabilities = {
19
- ...DEFAULT_CAPABILITIES,
20
- };
21
-
22
- // ─── ChromiumPlugin ───────────────────────────────────────────────
14
+ import { DEFAULT_CAPABILITIES } from "../../core/plugin-api.js";
23
15
 
24
16
  export class ChromiumPlugin extends PlaywrightPluginBase {
25
17
  readonly name = "chromium";
26
- readonly capabilities = CHROMIUM_CAPABILITIES;
18
+ readonly capabilities = DEFAULT_CAPABILITIES;
27
19
 
28
20
  /** Hardcoded Chrome user-agent — no dynamic capture needed. */
29
21
  protected get userAgent(): string {
@@ -23,8 +23,6 @@ Requires
23
23
  * Playwright Chromium browsers installed (``playwright install chromium``)
24
24
  """
25
25
 
26
- import sys
27
-
28
26
  from pi_browser_bridge.playwright_base import PlaywrightBridge, check_playwright_or_exit
29
27
 
30
28
 
@@ -4,9 +4,12 @@
4
4
  * Thin subclass of PlaywrightPluginBase. All shared logic lives in
5
5
  * backends/playwright-base/playwright-plugin.ts.
6
6
  *
7
+ * Launches Firefox directly with `firefox.launch()` and drives its own
8
+ * page; the plugin is the sole browser owner. No external-attach
9
+ * endpoint is exposed.
10
+ *
7
11
  * Uses probe-then-cache UA capture at first launch, with a hardcoded
8
- * fallback Firefox UA string. Launch args are empty — Firefox does
9
- * not accept Chromium sandbox flags.
12
+ * fallback Firefox UA string.
10
13
  */
11
14
 
12
15
  import { firefox } from "playwright";
@@ -47,9 +50,7 @@ export class FirefoxPlugin extends PlaywrightPluginBase {
47
50
  }
48
51
 
49
52
  protected async launchBrowser(): Promise<Browser> {
50
- return firefox.launch({
51
- headless: true,
52
- });
53
+ return firefox.launch({ headless: true });
53
54
  }
54
55
 
55
56
  protected get installHint(): string {
@@ -26,8 +26,6 @@ Requires
26
26
  * Playwright Firefox browsers installed (``playwright install firefox``)
27
27
  """
28
28
 
29
- import sys
30
-
31
29
  from pi_browser_bridge.playwright_base import PlaywrightBridge, check_playwright_or_exit
32
30
 
33
31
 
@@ -48,10 +46,13 @@ class FirefoxPyBridge(PlaywrightBridge):
48
46
  )
49
47
 
50
48
  def _launch_browser(self):
51
- """Launch a Firefox browser instance."""
52
- return self._pw.firefox.launch(
53
- headless=True,
54
- )
49
+ """Launch a Firefox browser instance.
50
+
51
+ Launches Firefox directly with ``firefox.launch()`` and drives its
52
+ own page; the plugin is the sole browser owner. No external-attach
53
+ endpoint is exposed.
54
+ """
55
+ return self._pw.firefox.launch(headless=True)
55
56
 
56
57
 
57
58
  # ═══════════════════════════════════════════════════════════════════════