pi-chrome 0.15.9 → 0.15.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable user-facing changes to `pi-chrome`.
4
4
 
5
+ ## 0.15.11 — 2026-05-14
6
+
7
+ - **README cleanup.** Removed the Playwright/CDP/Selenium comparison table and low-signal Composes with / Contributing sections from the package page because they are noisy and easy to drift.
8
+
9
+ ## 0.15.10 — 2026-05-14
10
+
11
+ - **Browser-side Chrome consent.** `/chrome authorize` now opens a Pi Chrome Connector approval page inside Chrome showing duration, workspace, process id, and extension/package versions. Chrome control unlocks only after the user approves there; denying, closing the tab, or timeout leaves control locked.
12
+ - **README cleanup.** Removed npm/download/license shield badges from the package page because they are noisy and easy to drift.
13
+
5
14
  ## 0.15.9 — 2026-05-14
6
15
 
7
16
  - **Tighter `/chrome` menu.** Removed the redundant “Connection status” item from the interactive `/chrome` menu because connection/auth/background are already shown in the menu header. `/chrome status` remains available as a slash command.
package/README.md CHANGED
@@ -12,10 +12,6 @@ Agent: chrome_tab(list) → chrome_snapshot(uid:…) → chrome_screenshot(...)
12
12
  You: [keeps coding — agent never asked you to log in]
13
13
  ```
14
14
 
15
- [![npm version](https://img.shields.io/npm/v/pi-chrome.svg)](https://www.npmjs.com/package/pi-chrome)
16
- [![npm downloads](https://img.shields.io/npm/dm/pi-chrome.svg)](https://www.npmjs.com/package/pi-chrome)
17
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
18
-
19
15
  `pi-chrome` ships **20+ browser tools** for Pi agents, backed by a small MIT-licensed Chrome extension that runs inside the Chrome profile **you already use** — including every site you're already signed into.
20
16
 
21
17
  ---
@@ -34,11 +30,11 @@ Then in Pi:
34
30
 
35
31
  On macOS this opens `chrome://extensions`, reveals the bundled `browser-extension/` folder in Finder, and copies its path to your clipboard. In Chrome: **Developer mode** → **Load unpacked** → paste the path. Done.
36
32
 
37
- Verify and authorize current Pi session:
33
+ Verify, then authorize current Pi session in Chrome:
38
34
 
39
35
  ```text
40
36
  /chrome doctor
41
- /chrome authorize
37
+ /chrome authorize # opens a Chrome approval page
42
38
  ```
43
39
 
44
40
  ```text
@@ -124,28 +120,6 @@ You: [files the ticket with the folder attached]
124
120
 
125
121
  ---
126
122
 
127
- ## Why pi-chrome vs. Playwright / CDP / Selenium
128
-
129
- > Short version: **pi-chrome is primitives — "Playwright for the Chrome you're already signed into."** Not an agent loop. Plug it under any agent framework (Browser Use, Stagehand, LangGraph) or call its tools directly from a Pi agent. See [docs/COMPARISON.md](./docs/COMPARISON.md) for the full three-axis landscape (drivers, agents, cloud providers).
130
-
131
- | | **pi-chrome** | Playwright / Puppeteer | CDP-based agents | Selenium / WebDriver |
132
- | ------------------------------ | --------------------------------- | ----------------------------- | ----------------------------- | ----------------------------- |
133
- | **Time from `pi install` → first useful action on your real account** | ~60s (load unpacked, `/chrome doctor`) | hours (script login, store creds, debug headless) | 30+ min (`--remote-debug` setup, attach) | hours (driver + login script) |
134
- | **Survives MFA / SSO without code** | ✅ already logged in | ❌ | ⚠️ if you re-auth | ❌ |
135
- | Uses your real signed-in Chrome | ✅ extension in your profile | ❌ throwaway profile | ⚠️ requires `--remote-debug` | ❌ throwaway profile |
136
- | Re-login required | **Never** | Every run | Sometimes | Every run |
137
- | **Multiple agents drive the same Chrome at once** | ✅ shared bridge | ❌ port collisions | ❌ | ❌ |
138
- | Watch agent work, live | ✅ default; run in background optional | ❌ headless or new window | ⚠️ debugger banner always | ❌ new window |
139
- | Real browser input | ✅ always for input tools | ✅ | ✅ | ✅ |
140
- | Network/console capture | ✅ built-in | ✅ | ✅ | ⚠️ via extensions |
141
- | **Honest result envelopes¹** | ✅ | ⚠️ | ❌ | ❌ |
142
- | Self-graded by built-in benchmark² | ✅ 38 primitives + 4 long-horizon | n/a | n/a | n/a |
143
-
144
- ¹ Every action returns `pageMutated`, `defaultPrevented`, `elementVisible`, `occludedBy`, and `valueMatches` so the agent knows when a click didn't take effect — instead of looping blindly.
145
- ² [`test-suite/`](./test-suite) grades browser-control primitives across input fidelity, activation gates, DOM complexity, and agent safety. If you build a competing tool, send a PR with your scores. We benchmark in public.
146
-
147
- ---
148
-
149
123
  ## Honest results
150
124
 
151
125
  Most browser-automation libraries return `void` or a generic ack. `pi-chrome` returns a structured envelope on every interaction:
@@ -188,7 +162,7 @@ Each tool is documented inline in Pi — agents see the parameters and gotchas (
188
162
 
189
163
  ### Authorization
190
164
 
191
- Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session:
165
+ Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session. `/chrome authorize` opens a browser-side approval page in Chrome; control unlocks only after you approve there.
192
166
 
193
167
  ```text
194
168
  /chrome authorize # default: authorize for 15 minutes
@@ -199,7 +173,7 @@ Chrome control is locked by default. Before any agent can use `chrome_*` tools,
199
173
  /chrome status # shows connection + auth + background
200
174
  ```
201
175
 
202
- This protects your signed-in Chrome profile from accidental agent use. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
176
+ This protects your signed-in Chrome profile from accidental agent use and makes the approval happen inside the browser profile being controlled. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
203
177
 
204
178
  ### Run in background / watch modes
205
179
 
@@ -265,7 +239,7 @@ If you build a competing tool, please open a PR with your scores. We benchmark i
265
239
 
266
240
  **Unpacked on purpose.** A Web Store extension cannot talk to a local bridge controlled by another tool on the same machine — so pi-chrome ships its bridge as an inspectable, MIT-licensed folder you load once with Developer Mode. Every line is yours to read in [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension). `/chrome doctor` reports the loaded extension version and warns when it drifts from your installed `pi-chrome`.
267
241
 
268
- The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi. Use `/chrome revoke` to lock them again.
242
+ The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi and approve the browser-side consent page in Chrome. Use `/chrome revoke` to lock them again.
269
243
 
270
244
  The Pi side listens on `127.0.0.1:17318` by default and rejects browser-origin command requests; ordinary web pages cannot use CORS to drive the bridge. Override before starting Pi:
271
245
 
@@ -277,14 +251,6 @@ There is no network exposure; the bridge binds to loopback only.
277
251
 
278
252
  ---
279
253
 
280
- ## Composes with
281
-
282
- - **[pi-qq](https://www.npmjs.com/package/pi-qq)** — `/qq summarize what the active GitHub tab shows` without polluting the main transcript.
283
- - **[pi-bar](https://www.npmjs.com/package/pi-bar)** — when the agent scrapes large pages, watch the context-usage segment turn yellow → red as a signal to `/qq` for a recap.
284
- - **PR demo skills** — screenshots write to `.pi/chrome-screenshots/` so you can attach them to PR descriptions or demo bundles.
285
-
286
- ---
287
-
288
254
  ## Roadmap signals
289
255
 
290
256
  `pi-chrome` is actively shipped. Things on the near roadmap:
@@ -298,16 +264,6 @@ If you want one of those next, open an issue.
298
264
 
299
265
  ---
300
266
 
301
- ## Contributing
302
-
303
- PRs welcome. The bar:
304
-
305
- 1. Add a benchmark page in `test-suite/` that fails before your change and passes after.
306
- 2. Keep `chrome_*` tool results honest — surface `pageMutated`, `valueMatches`, `defaultPrevented`, etc.
307
- 3. Don't break the "no re-login" guarantee. Anything that requires a fresh profile is out of scope.
308
-
309
- ---
310
-
311
267
  ## License
312
268
 
313
269
  MIT. See [LICENSE](./LICENSE).
package/SECURITY.md CHANGED
@@ -9,13 +9,13 @@ Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong
9
9
  `pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against:
10
10
 
11
11
  - Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.)
12
- - Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
12
+ - Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` plus browser-side consent in Chrome, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
13
13
 
14
14
  `pi-chrome` **is** designed to:
15
15
 
16
16
  - Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`).
17
17
  - Surface every action with an honest result envelope so the agent can't silently do the wrong thing.
18
- - Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session.
18
+ - Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session and approves the Chrome-side consent page.
19
19
  - Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome.
20
20
 
21
21
  ## The companion extension
@@ -26,7 +26,7 @@ The Chrome extension under `extensions/chrome-profile-bridge/browser-extension/`
26
26
 
27
27
  - Loopback bridge only. No remote port. No telemetry.
28
28
  - Chrome real input layer for interactive controls.
29
- - Chrome control locked by default; `/chrome authorize` unlocks current Pi session, `/chrome revoke` locks it again.
29
+ - Chrome control locked by default; `/chrome authorize` opens a Chrome consent page, approval unlocks current Pi session, `/chrome revoke` locks it again.
30
30
  - Run-in-background optional; tab/window focus is observable by default (the user can see Pi acting).
31
31
 
32
32
  ## Override the port
@@ -126,7 +126,7 @@ Yes — you can export cookies and replay them, or point Playwright at your exis
126
126
  Different security boundary, not strictly safer.
127
127
 
128
128
  - **CDP-based tools** require `chrome --remote-debugging-port=...`. That port is unauthenticated and exposes the whole browser to any local process. Easy to misconfigure.
129
- - **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session. **Only install the bundled extension if you trust the source you got the npm package from.**
129
+ - **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session and approved on the Chrome-side consent page. **Only install the bundled extension if you trust the source you got the npm package from.**
130
130
 
131
131
  If your threat model excludes extensions with broad permissions, neither approach is a fit — you want a sandboxed CI runner.
132
132
 
package/docs/EXAMPLES.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-chrome examples
2
2
 
3
- Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard` and `/chrome authorize`. Each one uses Chrome tabs and accounts you already have.
3
+ Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize` and approving in Chrome. Each one uses Chrome tabs and accounts you already have.
4
4
 
5
5
  ## Daily workflow
6
6
 
package/docs/FAQ.md CHANGED
@@ -26,11 +26,11 @@ That's Chrome's built-in warning when an extension uses `chrome.debugger`. pi-ch
26
26
 
27
27
  No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only and rejects browser-origin command requests, so ordinary web pages cannot use CORS to drive it.
28
28
 
29
- Chrome control is also locked per Pi session until you run `/chrome authorize`; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
29
+ Chrome control is also locked per Pi session until you run `/chrome authorize` and approve the Chrome-side consent page; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
30
30
 
31
31
  ## Can multiple Pi sessions use it at once?
32
32
 
33
- Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` before its chrome_* tools work.
33
+ Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` and approved in Chrome before its chrome_* tools work.
34
34
 
35
35
  ## Why can't this be on the Chrome Web Store?
36
36
 
@@ -0,0 +1,141 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ background: #0f172a;
5
+ color: #e5e7eb;
6
+ }
7
+
8
+ * { box-sizing: border-box; }
9
+
10
+ body {
11
+ margin: 0;
12
+ min-height: 100vh;
13
+ display: grid;
14
+ place-items: center;
15
+ padding: 32px;
16
+ background:
17
+ radial-gradient(circle at 20% 10%, rgba(79, 70, 229, 0.35), transparent 30%),
18
+ radial-gradient(circle at 80% 80%, rgba(14, 165, 233, 0.2), transparent 32%),
19
+ #0f172a;
20
+ }
21
+
22
+ .card {
23
+ width: min(680px, 100%);
24
+ background: rgba(15, 23, 42, 0.9);
25
+ border: 1px solid rgba(148, 163, 184, 0.25);
26
+ border-radius: 24px;
27
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.4);
28
+ padding: 32px;
29
+ }
30
+
31
+ .badge {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ border: 1px solid rgba(129, 140, 248, 0.45);
35
+ border-radius: 999px;
36
+ padding: 6px 12px;
37
+ color: #c7d2fe;
38
+ background: rgba(79, 70, 229, 0.18);
39
+ font-size: 13px;
40
+ font-weight: 700;
41
+ }
42
+
43
+ h1 {
44
+ margin: 18px 0 8px;
45
+ font-size: clamp(32px, 6vw, 48px);
46
+ line-height: 1;
47
+ }
48
+
49
+ .lead {
50
+ margin: 0 0 24px;
51
+ color: #cbd5e1;
52
+ font-size: 18px;
53
+ }
54
+
55
+ .details {
56
+ display: grid;
57
+ gap: 12px;
58
+ margin: 24px 0;
59
+ }
60
+
61
+ .details div {
62
+ display: grid;
63
+ grid-template-columns: 140px 1fr;
64
+ gap: 16px;
65
+ align-items: start;
66
+ padding: 14px 16px;
67
+ border-radius: 14px;
68
+ background: rgba(30, 41, 59, 0.72);
69
+ border: 1px solid rgba(148, 163, 184, 0.14);
70
+ }
71
+
72
+ .details span {
73
+ color: #94a3b8;
74
+ font-size: 13px;
75
+ font-weight: 700;
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.05em;
78
+ }
79
+
80
+ .details strong {
81
+ color: #f8fafc;
82
+ font-size: 15px;
83
+ overflow-wrap: anywhere;
84
+ }
85
+
86
+ .warning {
87
+ margin: 0 0 24px;
88
+ padding: 16px;
89
+ border-radius: 14px;
90
+ background: rgba(245, 158, 11, 0.12);
91
+ border: 1px solid rgba(245, 158, 11, 0.35);
92
+ color: #fde68a;
93
+ }
94
+
95
+ .actions {
96
+ display: flex;
97
+ justify-content: flex-end;
98
+ gap: 12px;
99
+ }
100
+
101
+ button {
102
+ border: 0;
103
+ border-radius: 12px;
104
+ padding: 12px 18px;
105
+ font: inherit;
106
+ font-weight: 800;
107
+ cursor: pointer;
108
+ }
109
+
110
+ button:focus-visible {
111
+ outline: 3px solid #93c5fd;
112
+ outline-offset: 2px;
113
+ }
114
+
115
+ .primary {
116
+ background: #4f46e5;
117
+ color: white;
118
+ }
119
+
120
+ .primary:hover { background: #4338ca; }
121
+
122
+ .secondary {
123
+ background: rgba(148, 163, 184, 0.16);
124
+ color: #e2e8f0;
125
+ }
126
+
127
+ .secondary:hover { background: rgba(148, 163, 184, 0.26); }
128
+
129
+ .status {
130
+ min-height: 20px;
131
+ margin: 18px 0 0;
132
+ color: #cbd5e1;
133
+ }
134
+
135
+ @media (max-width: 560px) {
136
+ body { padding: 16px; }
137
+ .card { padding: 22px; }
138
+ .details div { grid-template-columns: 1fr; gap: 6px; }
139
+ .actions { flex-direction: column-reverse; }
140
+ button { width: 100%; }
141
+ }
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Authorize pi-chrome</title>
7
+ <link rel="stylesheet" href="consent.css">
8
+ </head>
9
+ <body>
10
+ <main class="card">
11
+ <div class="badge">Pi Chrome Connector</div>
12
+ <h1>Authorize Chrome control?</h1>
13
+ <p class="lead">Pi is asking to inspect and control this Chrome profile.</p>
14
+
15
+ <section class="details" aria-label="Request details">
16
+ <div><span>Duration</span><strong id="duration">—</strong></div>
17
+ <div><span>Workspace</span><strong id="workspace">—</strong></div>
18
+ <div><span>Pi process</span><strong id="pid">—</strong></div>
19
+ <div><span>Extension</span><strong id="extension">—</strong></div>
20
+ </section>
21
+
22
+ <p class="warning">Approve only if you trust this Pi session and current task. Approved actions use your signed-in browser state and real Chrome input.</p>
23
+
24
+ <div class="actions">
25
+ <button id="deny" class="secondary" type="button">Deny</button>
26
+ <button id="approve" class="primary" type="button" autofocus>Authorize</button>
27
+ </div>
28
+
29
+ <p id="status" class="status" role="status"></p>
30
+ </main>
31
+ <script src="consent.js"></script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,75 @@
1
+ const params = new URLSearchParams(location.search);
2
+ const id = params.get("id") || "";
3
+
4
+ const els = {
5
+ duration: document.getElementById("duration"),
6
+ workspace: document.getElementById("workspace"),
7
+ pid: document.getElementById("pid"),
8
+ extension: document.getElementById("extension"),
9
+ approve: document.getElementById("approve"),
10
+ deny: document.getElementById("deny"),
11
+ status: document.getElementById("status"),
12
+ };
13
+
14
+ function setStatus(text) {
15
+ els.status.textContent = text || "";
16
+ }
17
+
18
+ function setDisabled(disabled) {
19
+ els.approve.disabled = disabled;
20
+ els.deny.disabled = disabled;
21
+ }
22
+
23
+ function render(request) {
24
+ els.duration.textContent = request.durationLabel || "—";
25
+ els.workspace.textContent = request.workspace || "unknown workspace";
26
+ els.pid.textContent = request.pid ? String(request.pid) : "unknown";
27
+ const versions = [];
28
+ if (request.extensionVersion) versions.push(`extension ${request.extensionVersion}`);
29
+ if (request.piChromeVersion) versions.push(`pi-chrome ${request.piChromeVersion}`);
30
+ els.extension.textContent = versions.join(" · ") || request.extensionId || "—";
31
+ }
32
+
33
+ async function send(message) {
34
+ return await chrome.runtime.sendMessage(message);
35
+ }
36
+
37
+ async function decide(approved) {
38
+ setDisabled(true);
39
+ setStatus(approved ? "Authorizing…" : "Denying…");
40
+ try {
41
+ const response = await send({ type: "piChromeConsentDecision", id, approved });
42
+ if (!response?.ok) throw new Error(response?.error || "Consent request failed");
43
+ setStatus(approved ? "Authorized. You can close this tab." : "Denied. You can close this tab.");
44
+ window.close();
45
+ } catch (error) {
46
+ setDisabled(false);
47
+ setStatus(error?.message || String(error));
48
+ }
49
+ }
50
+
51
+ async function init() {
52
+ if (!id) {
53
+ setDisabled(true);
54
+ setStatus("Missing consent request id.");
55
+ return;
56
+ }
57
+ setStatus("Loading request…");
58
+ try {
59
+ const response = await send({ type: "piChromeConsentGet", id });
60
+ if (!response?.ok) throw new Error(response?.error || "Consent request not found");
61
+ render(response.request || {});
62
+ setStatus("");
63
+ } catch (error) {
64
+ setDisabled(true);
65
+ setStatus(error?.message || String(error));
66
+ }
67
+ }
68
+
69
+ els.approve.addEventListener("click", () => decide(true));
70
+ els.deny.addEventListener("click", () => decide(false));
71
+ document.addEventListener("keydown", (event) => {
72
+ if (event.key === "Escape") decide(false);
73
+ });
74
+
75
+ void init();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.9",
4
+ "version": "0.15.11",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -1,7 +1,10 @@
1
1
  const BRIDGE_URL = "http://127.0.0.1:17318";
2
2
  const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`;
3
3
  const POLL_ERROR_BACKOFF_MS = 2000;
4
+ const CONSENT_TIMEOUT_MS = 5 * 60 * 1000;
4
5
  let polling = false;
6
+ let nextConsentRequestId = 1;
7
+ const pendingConsentRequests = new Map(); // id -> { request, resolve, timer, tabId }
5
8
 
6
9
  // =================== Chrome input (CDP) layer ===================
7
10
  // Tracks which tabs we have attached chrome.debugger to.
@@ -591,6 +594,82 @@ async function chromeInputUpload(params) {
591
594
  // ===============================================================
592
595
 
593
596
 
597
+ function consentRequestSnapshot(id, request) {
598
+ return {
599
+ id,
600
+ extensionVersion: chrome.runtime.getManifest().version,
601
+ extensionId: chrome.runtime.id,
602
+ workspace: String(request.workspace || ""),
603
+ pid: request.pid ?? null,
604
+ durationLabel: String(request.durationLabel || "15 minutes"),
605
+ requestedAt: request.requestedAt || Date.now(),
606
+ piChromeVersion: String(request.piChromeVersion || ""),
607
+ };
608
+ }
609
+
610
+ async function requestBrowserConsent(params) {
611
+ const id = String(nextConsentRequestId++);
612
+ const request = {
613
+ ...params,
614
+ requestedAt: Date.now(),
615
+ };
616
+ const url = chrome.runtime.getURL(`consent.html?id=${encodeURIComponent(id)}`);
617
+ const decision = new Promise((resolve) => {
618
+ const finish = (approved, reason) => {
619
+ const pending = pendingConsentRequests.get(id);
620
+ if (!pending) return;
621
+ pendingConsentRequests.delete(id);
622
+ clearTimeout(pending.timer);
623
+ if (pending.tabId) chrome.tabs.remove(pending.tabId).catch(() => undefined);
624
+ resolve({ approved, reason, id, decidedAt: Date.now() });
625
+ };
626
+ const timer = setTimeout(() => finish(false, "timed out waiting for browser approval"), CONSENT_TIMEOUT_MS);
627
+ pendingConsentRequests.set(id, { request, resolve: finish, timer, tabId: null });
628
+ });
629
+ try {
630
+ const tab = await chrome.tabs.create({ url, active: true });
631
+ if (tab.windowId !== undefined) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => undefined);
632
+ const pending = pendingConsentRequests.get(id);
633
+ if (pending) pending.tabId = tab.id;
634
+ } catch (error) {
635
+ const pending = pendingConsentRequests.get(id);
636
+ if (pending) pending.resolve(false, `could not open consent tab: ${error?.message || error}`);
637
+ }
638
+ return await decision;
639
+ }
640
+
641
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
642
+ if (!message || typeof message !== "object") return false;
643
+ if (message.type === "piChromeConsentGet") {
644
+ const id = String(message.id || "");
645
+ const pending = pendingConsentRequests.get(id);
646
+ sendResponse(pending ? { ok: true, request: consentRequestSnapshot(id, pending.request) } : { ok: false, error: "Consent request expired or not found" });
647
+ return true;
648
+ }
649
+ if (message.type === "piChromeConsentDecision") {
650
+ const id = String(message.id || "");
651
+ const pending = pendingConsentRequests.get(id);
652
+ if (!pending) {
653
+ sendResponse({ ok: false, error: "Consent request expired or not found" });
654
+ return true;
655
+ }
656
+ pending.resolve(message.approved === true, message.approved === true ? "approved in Chrome" : "denied in Chrome");
657
+ sendResponse({ ok: true });
658
+ return true;
659
+ }
660
+ return false;
661
+ });
662
+
663
+ chrome.tabs.onRemoved.addListener((tabId) => {
664
+ for (const [id, pending] of pendingConsentRequests) {
665
+ if (pending.tabId === tabId) {
666
+ pending.resolve(false, "consent tab closed");
667
+ pendingConsentRequests.delete(id);
668
+ clearTimeout(pending.timer);
669
+ }
670
+ }
671
+ });
672
+
594
673
  function armKeepaliveAlarm() {
595
674
  chrome.alarms.create("pi-bridge-keepalive", { periodInMinutes: 0.5 });
596
675
  }
@@ -686,6 +765,8 @@ async function dispatch(action, params) {
686
765
  bridgeUrl: BRIDGE_URL,
687
766
  userAgent: navigator.userAgent,
688
767
  };
768
+ case "consent.request":
769
+ return requestBrowserConsent(params);
689
770
  case "tab.list":
690
771
  return (await chrome.tabs.query({})).map(formatTab);
691
772
  case "tab.new": {
@@ -609,12 +609,25 @@ Usage rules:
609
609
  };
610
610
 
611
611
  const authorizeFor = async (ctx: ExtensionContext, label: string, until: number | "indefinite") => {
612
- const ok = await ctx.ui.confirm(
613
- "Authorize pi-chrome control?",
614
- `This Pi session will be allowed to inspect and control your existing Chrome profile for ${label}.\n\nChrome actions use your signed-in browser state and real input. Only approve if you trust the current agent/task.`,
615
- );
616
- if (!ok) {
617
- ctx.ui.notify("Chrome control remains locked.", "info");
612
+ ctx.ui.notify("Opening Chrome approval page…", "info");
613
+ let consent: { approved?: boolean; reason?: string };
614
+ try {
615
+ consent = (await bridge.send("consent.request", {
616
+ durationLabel: label,
617
+ workspace: workspaceCwd(ctx),
618
+ pid: process.pid,
619
+ piChromeVersion: PI_CHROME_VERSION,
620
+ }, 5 * 60_000 + 5_000)) as { approved?: boolean; reason?: string };
621
+ } catch (error) {
622
+ const message = (error as Error).message;
623
+ const hint = message.includes("Unknown action: consent.request")
624
+ ? "Open chrome://extensions and reload 'Pi Chrome Connector', then try /chrome authorize again."
625
+ : "Run /chrome doctor if the companion extension is not responding.";
626
+ ctx.ui.notify(`Chrome approval failed: ${message}\n${hint}`, "warning");
627
+ return;
628
+ }
629
+ if (!consent.approved) {
630
+ ctx.ui.notify(`Chrome control remains locked${consent.reason ? ` (${consent.reason})` : ""}.`, "info");
618
631
  return;
619
632
  }
620
633
  chromeAuthorizedUntil = until;
@@ -743,7 +756,7 @@ Usage rules:
743
756
 
744
757
  pi.registerCommand("chrome", {
745
758
  description:
746
- "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
759
+ "All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — open Chrome approval and allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
747
760
  getArgumentCompletions: (prefix) => {
748
761
  const raw = prefix;
749
762
  const trimmedRight = raw.replace(/\s+$/, "");
@@ -760,7 +773,7 @@ Usage rules:
760
773
  let candidates: Item[] = [];
761
774
  if (path.length === 0) {
762
775
  candidates = [
763
- { fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
776
+ { fullValue: "authorize", label: "authorize", description: "Open Chrome approval and allow this Pi session to use chrome_* tools." },
764
777
  { fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
765
778
  { fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
766
779
  { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"