pi-chrome 0.15.8 → 0.15.10
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 +9 -0
- package/README.md +5 -9
- package/SECURITY.md +3 -3
- package/docs/COMPARISON.md +1 -1
- package/docs/EXAMPLES.md +1 -1
- package/docs/FAQ.md +2 -2
- package/extensions/chrome-profile-bridge/browser-extension/consent.css +141 -0
- package/extensions/chrome-profile-bridge/browser-extension/consent.html +33 -0
- package/extensions/chrome-profile-bridge/browser-extension/consent.js +75 -0
- package/extensions/chrome-profile-bridge/browser-extension/manifest.json +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/service_worker.js +81 -0
- package/extensions/chrome-profile-bridge/index.ts +21 -10
- package/package.json +1 -1
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.10 — 2026-05-14
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **README cleanup.** Removed npm/download/license shield badges from the package page because they are noisy and easy to drift.
|
|
9
|
+
|
|
10
|
+
## 0.15.9 — 2026-05-14
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
|
|
5
14
|
## 0.15.8 — 2026-05-14
|
|
6
15
|
|
|
7
16
|
- **Simpler `/chrome` submenus.** Authorize menu now offers 15 minutes, 30 minutes, indefinite, and custom minutes only. Background menu now offers only foreground/background. Esc from a submenu returns to the main `/chrome` menu.
|
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
|
-
[](https://www.npmjs.com/package/pi-chrome)
|
|
16
|
-
[](https://www.npmjs.com/package/pi-chrome)
|
|
17
|
-
[](./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
|
|
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
|
|
@@ -188,7 +184,7 @@ Each tool is documented inline in Pi — agents see the parameters and gotchas (
|
|
|
188
184
|
|
|
189
185
|
### Authorization
|
|
190
186
|
|
|
191
|
-
Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session
|
|
187
|
+
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
188
|
|
|
193
189
|
```text
|
|
194
190
|
/chrome authorize # default: authorize for 15 minutes
|
|
@@ -199,7 +195,7 @@ Chrome control is locked by default. Before any agent can use `chrome_*` tools,
|
|
|
199
195
|
/chrome status # shows connection + auth + background
|
|
200
196
|
```
|
|
201
197
|
|
|
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.
|
|
198
|
+
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
199
|
|
|
204
200
|
### Run in background / watch modes
|
|
205
201
|
|
|
@@ -265,7 +261,7 @@ If you build a competing tool, please open a PR with your scores. We benchmark i
|
|
|
265
261
|
|
|
266
262
|
**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
263
|
|
|
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.
|
|
264
|
+
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
265
|
|
|
270
266
|
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
267
|
|
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`
|
|
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
|
package/docs/COMPARISON.md
CHANGED
|
@@ -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
|
|
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
|
|
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,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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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;
|
|
@@ -726,7 +739,6 @@ Usage rules:
|
|
|
726
739
|
const choice = await ctx.ui.select(`pi-chrome\n${await statusSummary()}`, [
|
|
727
740
|
"Authorize Chrome control…",
|
|
728
741
|
"Lock Chrome control",
|
|
729
|
-
"Connection status",
|
|
730
742
|
"Doctor / troubleshoot",
|
|
731
743
|
"Background / watch mode…",
|
|
732
744
|
"Install / onboard extension",
|
|
@@ -735,7 +747,6 @@ Usage rules:
|
|
|
735
747
|
switch (choice) {
|
|
736
748
|
case "Authorize Chrome control…": await openAuthorizeMenu(ctx); continue;
|
|
737
749
|
case "Lock Chrome control": return revokeHandler(ctx);
|
|
738
|
-
case "Connection status": return statusHandler(ctx);
|
|
739
750
|
case "Doctor / troubleshoot": return doctorHandler(ctx);
|
|
740
751
|
case "Background / watch mode…": await openBackgroundMenu(ctx); continue;
|
|
741
752
|
case "Install / onboard extension": return onboardHandler(ctx);
|
|
@@ -745,7 +756,7 @@ Usage rules:
|
|
|
745
756
|
|
|
746
757
|
pi.registerCommand("chrome", {
|
|
747
758
|
description:
|
|
748
|
-
"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.",
|
|
749
760
|
getArgumentCompletions: (prefix) => {
|
|
750
761
|
const raw = prefix;
|
|
751
762
|
const trimmedRight = raw.replace(/\s+$/, "");
|
|
@@ -762,7 +773,7 @@ Usage rules:
|
|
|
762
773
|
let candidates: Item[] = [];
|
|
763
774
|
if (path.length === 0) {
|
|
764
775
|
candidates = [
|
|
765
|
-
{ fullValue: "authorize", label: "authorize", description: "
|
|
776
|
+
{ fullValue: "authorize", label: "authorize", description: "Open Chrome approval and allow this Pi session to use chrome_* tools." },
|
|
766
777
|
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
|
|
767
778
|
{ fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
|
|
768
779
|
{ fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|