pi-browser-use 0.6.1 → 0.7.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 (72) hide show
  1. package/README.md +39 -11
  2. package/dist/auth-verifiers.d.ts +60 -0
  3. package/dist/auth-verifiers.d.ts.map +1 -0
  4. package/dist/auth-verifiers.js +92 -0
  5. package/dist/auth-verifiers.js.map +1 -0
  6. package/dist/chrome-launcher.d.ts +100 -0
  7. package/dist/chrome-launcher.d.ts.map +1 -0
  8. package/dist/chrome-launcher.js +268 -0
  9. package/dist/chrome-launcher.js.map +1 -0
  10. package/dist/config.d.ts +3 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +28 -5
  13. package/dist/config.js.map +1 -1
  14. package/dist/doctor.d.ts +8 -1
  15. package/dist/doctor.d.ts.map +1 -1
  16. package/dist/doctor.js +16 -6
  17. package/dist/doctor.js.map +1 -1
  18. package/dist/existing-flow.d.ts +68 -0
  19. package/dist/existing-flow.d.ts.map +1 -0
  20. package/dist/existing-flow.js +128 -0
  21. package/dist/existing-flow.js.map +1 -0
  22. package/dist/focus-policy.d.ts +28 -0
  23. package/dist/focus-policy.d.ts.map +1 -0
  24. package/dist/focus-policy.js +28 -0
  25. package/dist/focus-policy.js.map +1 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +494 -39
  29. package/dist/index.js.map +1 -1
  30. package/dist/named-profile.d.ts +47 -0
  31. package/dist/named-profile.d.ts.map +1 -0
  32. package/dist/named-profile.js +123 -0
  33. package/dist/named-profile.js.map +1 -0
  34. package/dist/persistent-backend.d.ts +77 -0
  35. package/dist/persistent-backend.d.ts.map +1 -0
  36. package/dist/persistent-backend.js +166 -0
  37. package/dist/persistent-backend.js.map +1 -0
  38. package/dist/persistent-store.d.ts +35 -0
  39. package/dist/persistent-store.d.ts.map +1 -0
  40. package/dist/persistent-store.js +98 -0
  41. package/dist/persistent-store.js.map +1 -0
  42. package/dist/profile-lock.d.ts +40 -0
  43. package/dist/profile-lock.d.ts.map +1 -0
  44. package/dist/profile-lock.js +165 -0
  45. package/dist/profile-lock.js.map +1 -0
  46. package/dist/profile.d.ts +3 -1
  47. package/dist/profile.d.ts.map +1 -1
  48. package/dist/profile.js +15 -6
  49. package/dist/profile.js.map +1 -1
  50. package/dist/session-manager.d.ts +125 -0
  51. package/dist/session-manager.d.ts.map +1 -0
  52. package/dist/session-manager.js +275 -0
  53. package/dist/session-manager.js.map +1 -0
  54. package/dist/session.d.ts +92 -0
  55. package/dist/session.d.ts.map +1 -0
  56. package/dist/session.js +29 -0
  57. package/dist/session.js.map +1 -0
  58. package/dist/setup-flow.d.ts +78 -0
  59. package/dist/setup-flow.d.ts.map +1 -0
  60. package/dist/setup-flow.js +97 -0
  61. package/dist/setup-flow.js.map +1 -0
  62. package/dist/tab-bridge.d.ts +71 -0
  63. package/dist/tab-bridge.d.ts.map +1 -0
  64. package/dist/tab-bridge.js +198 -0
  65. package/dist/tab-bridge.js.map +1 -0
  66. package/extension/README.md +33 -0
  67. package/extension/background.js +185 -0
  68. package/extension/manifest.json +12 -0
  69. package/package.json +2 -1
  70. package/skills/auth-bootstrap/SKILL.md +13 -0
  71. package/skills/browser-policy/SKILL.md +29 -4
  72. package/skills/gmail-auth/SKILL.md +51 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Existing-mode tab broker bridge (spec section 18).
3
+ *
4
+ * Pi cannot drive the extension directly, and raw MCP `new_page` calls would
5
+ * create unmanaged foreground tabs — so tab creation goes through a tiny
6
+ * loopback HTTP bridge the extension polls:
7
+ *
8
+ * ```text
9
+ * Agent → BrowserSession.openPage(url) → ExistingSession
10
+ * → POST /v1/request {url} → token
11
+ * → extension polls GET /v1/pending → openPiTab(url) (inactive, grouped)
12
+ * → extension POSTs /v1/complete {token, tabId}
13
+ * → Pi waits for the token, then MCP selects the tab — never activating it
14
+ * ```
15
+ *
16
+ * Correlation is by unique token, never "first tab whose URL matches"
17
+ * (duplicate Gmail/GitHub tabs are common). Requests expire (default 2 min)
18
+ * so a dead extension cannot leak queue entries. Only Pi-owned tabs are
19
+ * tracked, so session-end cleanup never touches user tabs (spec 19).
20
+ *
21
+ * Security: binds 127.0.0.1 only and rejects non-loopback remotes.
22
+ */
23
+ export declare const DEFAULT_BRIDGE_PORT = 31973;
24
+ export interface TabRequest {
25
+ url: string;
26
+ token: string;
27
+ createdAt: number;
28
+ }
29
+ export interface TabCompletion {
30
+ token: string;
31
+ tabId?: number;
32
+ windowId?: number;
33
+ error?: string;
34
+ completedAt: number;
35
+ }
36
+ export interface TabBridgeOptions {
37
+ port?: number;
38
+ /** Pending requests older than this are dropped. Default 2 minutes. */
39
+ requestTtlMs?: number;
40
+ /** Completed entries kept for late waiters. Default 5 minutes. */
41
+ completionTtlMs?: number;
42
+ }
43
+ export declare class TabBridge {
44
+ private readonly options;
45
+ private server;
46
+ private pending;
47
+ private completed;
48
+ private readonly requestTtlMs;
49
+ private readonly completionTtlMs;
50
+ private sweepTimer;
51
+ constructor(options?: TabBridgeOptions);
52
+ port(): number | undefined;
53
+ baseUrl(): string;
54
+ start(): Promise<string>;
55
+ stop(): Promise<void>;
56
+ /** Enqueue an open-tab request; returns the correlation token. */
57
+ requestTab(url: string, token?: string): string;
58
+ /** Wait for the extension to complete `token` (throws on timeout/error). */
59
+ waitForTab(token: string, options?: {
60
+ timeoutMs?: number;
61
+ pollMs?: number;
62
+ signal?: AbortSignal;
63
+ }): Promise<TabCompletion>;
64
+ /** Tab IDs Pi created and still owns (session-end cleanup scope). */
65
+ ownedTabIds(): number[];
66
+ pendingCount(): number;
67
+ private sweep;
68
+ private json;
69
+ private handle;
70
+ }
71
+ //# sourceMappingURL=tab-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-bridge.d.ts","sourceRoot":"","sources":["../src/tab-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,eAAO,MAAM,mBAAmB,QAAQ,CAAA;AAExC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAgCD,qBAAa,SAAS;IAQR,OAAO,CAAC,QAAQ,CAAC,OAAO;IAPpC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,SAAS,CAAmC;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,UAAU,CAA4C;gBAEjC,OAAO,GAAE,gBAAqB;IAK3D,IAAI,IAAI,MAAM,GAAG,SAAS;IAK1B,OAAO,IAAI,MAAM;IAIX,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAgBxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,kEAAkE;IAClE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,MAAqB,GAAG,MAAM;IAO7D,4EAA4E;IACtE,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACtE,OAAO,CAAC,aAAa,CAAC;IAkBzB,qEAAqE;IACrE,WAAW,IAAI,MAAM,EAAE;IAMvB,YAAY,IAAI,MAAM;IAKtB,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,IAAI;YAKE,MAAM;CAmDrB"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Existing-mode tab broker bridge (spec section 18).
3
+ *
4
+ * Pi cannot drive the extension directly, and raw MCP `new_page` calls would
5
+ * create unmanaged foreground tabs — so tab creation goes through a tiny
6
+ * loopback HTTP bridge the extension polls:
7
+ *
8
+ * ```text
9
+ * Agent → BrowserSession.openPage(url) → ExistingSession
10
+ * → POST /v1/request {url} → token
11
+ * → extension polls GET /v1/pending → openPiTab(url) (inactive, grouped)
12
+ * → extension POSTs /v1/complete {token, tabId}
13
+ * → Pi waits for the token, then MCP selects the tab — never activating it
14
+ * ```
15
+ *
16
+ * Correlation is by unique token, never "first tab whose URL matches"
17
+ * (duplicate Gmail/GitHub tabs are common). Requests expire (default 2 min)
18
+ * so a dead extension cannot leak queue entries. Only Pi-owned tabs are
19
+ * tracked, so session-end cleanup never touches user tabs (spec 19).
20
+ *
21
+ * Security: binds 127.0.0.1 only and rejects non-loopback remotes.
22
+ */
23
+ import { createServer } from 'node:http';
24
+ import { randomUUID } from 'node:crypto';
25
+ export const DEFAULT_BRIDGE_PORT = 31973;
26
+ function isLoopback(remoteAddress) {
27
+ return (remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1');
28
+ }
29
+ function readJsonBody(req, limit = 64 * 1024) {
30
+ return new Promise((resolve, reject) => {
31
+ const chunks = [];
32
+ let size = 0;
33
+ req.on('data', (chunk) => {
34
+ size += chunk.length;
35
+ if (size > limit) {
36
+ reject(new Error('Request body too large.'));
37
+ req.destroy();
38
+ return;
39
+ }
40
+ chunks.push(chunk);
41
+ });
42
+ req.on('end', () => {
43
+ try {
44
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
45
+ }
46
+ catch {
47
+ reject(new Error('Invalid JSON body.'));
48
+ }
49
+ });
50
+ req.on('error', reject);
51
+ });
52
+ }
53
+ export class TabBridge {
54
+ options;
55
+ server;
56
+ pending = new Map();
57
+ completed = new Map();
58
+ requestTtlMs;
59
+ completionTtlMs;
60
+ sweepTimer;
61
+ constructor(options = {}) {
62
+ this.options = options;
63
+ this.requestTtlMs = options.requestTtlMs ?? 2 * 60 * 1000;
64
+ this.completionTtlMs = options.completionTtlMs ?? 5 * 60 * 1000;
65
+ }
66
+ port() {
67
+ const address = this.server?.address();
68
+ return typeof address === 'object' && address ? address.port : undefined;
69
+ }
70
+ baseUrl() {
71
+ return `http://127.0.0.1:${this.port()}`;
72
+ }
73
+ async start() {
74
+ if (this.server)
75
+ return this.baseUrl();
76
+ this.server = createServer((req, res) => {
77
+ void this.handle(req, res).catch(() => {
78
+ this.json(res, 500, { error: 'Internal bridge error.' });
79
+ });
80
+ });
81
+ await new Promise((resolve, reject) => {
82
+ this.server.on('error', reject);
83
+ this.server.listen(this.options.port ?? DEFAULT_BRIDGE_PORT, '127.0.0.1', () => resolve());
84
+ });
85
+ this.sweepTimer = setInterval(() => this.sweep(), 30_000);
86
+ this.sweepTimer.unref?.();
87
+ return this.baseUrl();
88
+ }
89
+ async stop() {
90
+ if (this.sweepTimer)
91
+ clearInterval(this.sweepTimer);
92
+ this.sweepTimer = undefined;
93
+ if (!this.server)
94
+ return;
95
+ const server = this.server;
96
+ this.server = undefined;
97
+ await new Promise((resolve) => server.close(() => resolve()));
98
+ }
99
+ /** Enqueue an open-tab request; returns the correlation token. */
100
+ requestTab(url, token = randomUUID()) {
101
+ if (typeof url !== 'string' || url.length === 0)
102
+ throw new Error('Tab URL is required.');
103
+ this.sweep();
104
+ this.pending.set(token, { url, token, createdAt: Date.now() });
105
+ return token;
106
+ }
107
+ /** Wait for the extension to complete `token` (throws on timeout/error). */
108
+ async waitForTab(token, options) {
109
+ const timeoutMs = options?.timeoutMs ?? 30_000;
110
+ const pollMs = options?.pollMs ?? 100;
111
+ const deadline = Date.now() + timeoutMs;
112
+ while (Date.now() < deadline) {
113
+ if (options?.signal?.aborted)
114
+ throw new Error('Tab wait aborted.');
115
+ const done = this.completed.get(token);
116
+ if (done) {
117
+ this.completed.delete(token);
118
+ if (done.error)
119
+ throw new Error(`Extension failed to open tab: ${done.error}`);
120
+ return done;
121
+ }
122
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
123
+ }
124
+ this.pending.delete(token);
125
+ throw new Error(`Timed out waiting for the extension to open the tab (token ${token}).`);
126
+ }
127
+ /** Tab IDs Pi created and still owns (session-end cleanup scope). */
128
+ ownedTabIds() {
129
+ return [...this.completed.values()]
130
+ .filter((c) => c.tabId !== undefined && !c.error)
131
+ .map((c) => c.tabId);
132
+ }
133
+ pendingCount() {
134
+ this.sweep();
135
+ return this.pending.size;
136
+ }
137
+ sweep() {
138
+ const now = Date.now();
139
+ for (const [token, req] of this.pending) {
140
+ if (now - req.createdAt > this.requestTtlMs)
141
+ this.pending.delete(token);
142
+ }
143
+ for (const [token, done] of this.completed) {
144
+ if (now - done.completedAt > this.completionTtlMs)
145
+ this.completed.delete(token);
146
+ }
147
+ }
148
+ json(res, status, body) {
149
+ res.writeHead(status, { 'content-type': 'application/json' });
150
+ res.end(JSON.stringify(body));
151
+ }
152
+ async handle(req, res) {
153
+ if (!isLoopback(req.socket.remoteAddress)) {
154
+ this.json(res, 403, { error: 'Tab bridge accepts loopback connections only.' });
155
+ return;
156
+ }
157
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
158
+ if (req.method === 'GET' && url.pathname === '/v1/pending') {
159
+ this.sweep();
160
+ this.json(res, 200, { requests: [...this.pending.values()] });
161
+ this.pending.clear();
162
+ return;
163
+ }
164
+ if (req.method === 'POST' && url.pathname === '/v1/request') {
165
+ const body = (await readJsonBody(req));
166
+ if (typeof body.url !== 'string' || body.url.length === 0) {
167
+ this.json(res, 400, { error: 'Field "url" is required.' });
168
+ return;
169
+ }
170
+ const token = typeof body.token === 'string' && body.token.length > 0 ? body.token : randomUUID();
171
+ this.requestTab(body.url, token);
172
+ this.json(res, 200, { token });
173
+ return;
174
+ }
175
+ if (req.method === 'POST' && url.pathname === '/v1/complete') {
176
+ const body = (await readJsonBody(req));
177
+ if (typeof body.token !== 'string' || body.token.length === 0) {
178
+ this.json(res, 400, { error: 'Field "token" is required.' });
179
+ return;
180
+ }
181
+ this.completed.set(body.token, {
182
+ token: body.token,
183
+ tabId: typeof body.tabId === 'number' ? body.tabId : undefined,
184
+ windowId: typeof body.windowId === 'number' ? body.windowId : undefined,
185
+ error: typeof body.error === 'string' ? body.error : undefined,
186
+ completedAt: Date.now(),
187
+ });
188
+ this.json(res, 200, { ok: true });
189
+ return;
190
+ }
191
+ if (req.method === 'GET' && url.pathname === '/v1/health') {
192
+ this.json(res, 200, { ok: true });
193
+ return;
194
+ }
195
+ this.json(res, 404, { error: 'Unknown tab bridge route.' });
196
+ }
197
+ }
198
+ //# sourceMappingURL=tab-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab-bridge.js","sourceRoot":"","sources":["../src/tab-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAA;AAChG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAA;AAwBxC,SAAS,UAAU,CAAC,aAAiC;IACnD,OAAO,CACL,aAAa,KAAK,WAAW,IAAI,aAAa,KAAK,KAAK,IAAI,aAAa,KAAK,kBAAkB,CACjG,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,GAAoB,EAAE,KAAK,GAAG,EAAE,GAAG,IAAI;IAC3D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,IAAI,IAAI,GAAG,CAAC,CAAA;QACZ,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC/B,IAAI,IAAI,KAAK,CAAC,MAAM,CAAA;YACpB,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAA;gBAC5C,GAAG,CAAC,OAAO,EAAE,CAAA;gBACb,OAAM;YACR,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC,CAAC,CAAA;QACF,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YAC7D,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAA;YACzC,CAAC;QACH,CAAC,CAAC,CAAA;QACF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IACzB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,OAAO,SAAS;IAQS;IAPrB,MAAM,CAAoB;IAC1B,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAA;IACvC,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAA;IACnC,YAAY,CAAQ;IACpB,eAAe,CAAQ;IAChC,UAAU,CAA4C;IAE9D,YAA6B,UAA4B,EAAE;QAA9B,YAAO,GAAP,OAAO,CAAuB;QACzD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;QACzD,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;IACjE,CAAC;IAED,IAAI;QACF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAA;QACtC,OAAO,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1E,CAAC;IAED,OAAO;QACL,OAAO,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,CAAA;IAC1C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,OAAO,EAAE,CAAA;QACtC,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACtC,KAAK,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBACpC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAA;YAC1D,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QACF,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,IAAI,CAAC,MAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;YAChC,IAAI,CAAC,MAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,mBAAmB,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;QAC7F,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,CAAA;QACzD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,CAAA;QACzB,OAAO,IAAI,CAAC,OAAO,EAAE,CAAA;IACvB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,IAAI,CAAC,UAAU;YAAE,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACnD,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;QAC3B,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAM;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,kEAAkE;IAClE,UAAU,CAAC,GAAW,EAAE,QAAgB,UAAU,EAAE;QAClD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;QACxF,IAAI,CAAC,KAAK,EAAE,CAAA;QACZ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC9D,OAAO,KAAK,CAAA;IACd,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,UAAU,CACd,KAAa,EACb,OAAuE;QAEvE,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,MAAM,CAAA;QAC9C,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,GAAG,CAAA;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAA;QACvC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC7B,IAAI,OAAO,EAAE,MAAM,EAAE,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAA;YAClE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACtC,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAC5B,IAAI,IAAI,CAAC,KAAK;oBAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;gBAC9E,OAAO,IAAI,CAAA;YACb,CAAC;YACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;QAC7D,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,MAAM,IAAI,KAAK,CAAC,8DAA8D,KAAK,IAAI,CAAC,CAAA;IAC1F,CAAC;IAED,qEAAqE;IACrE,WAAW;QACT,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;aAChC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;aAChD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAe,CAAC,CAAA;IAClC,CAAC;IAED,YAAY;QACV,IAAI,CAAC,KAAK,EAAE,CAAA;QACZ,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;IAEO,KAAK;QACX,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxC,IAAI,GAAG,GAAG,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,YAAY;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACzE,CAAC;QACD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,IAAI,GAAG,GAAG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,eAAe;gBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACjF,CAAC;IACH,CAAC;IAEO,IAAI,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa;QAC7D,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAA;QAC7D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,GAAoB,EAAE,GAAmB;QAC5D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAA;YAC/E,OAAM;QACR,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAA;QACvD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC3D,IAAI,CAAC,KAAK,EAAE,CAAA;YACZ,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;YAC7D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;YACpB,OAAM;QACR,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC5D,MAAM,IAAI,GAAG,CAAC,MAAM,YAAY,CAAC,GAAG,CAAC,CAAuC,CAAA;YAC5E,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAA;gBAC1D,OAAM;YACR,CAAC;YACD,MAAM,KAAK,GACT,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,CAAA;YACrF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAChC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;YAC9B,OAAM;QACR,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,cAAc,EAAE,CAAC;YAC7D,MAAM,IAAI,GAAG,CAAC,MAAM,YAAY,CAAC,GAAG,CAAC,CAKpC,CAAA;YACD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC9D,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC,CAAA;gBAC5D,OAAM;YACR,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE;gBAC7B,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBAC9D,QAAQ,EAAE,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;gBACvE,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;gBAC9D,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE;aACxB,CAAC,CAAA;YACF,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAA;IAC7D,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ # Pi extension — Existing-mode tab broker
2
+
3
+ Load this unpacked extension into your daily Chrome to let Pi open
4
+ background tabs in the collapsed `pi-browser-use` group (spec sections
5
+ 14–18). Without it, `browser_open_background_tab` fails clearly instead of
6
+ opening unmanaged foreground tabs.
7
+
8
+ ## Install (load unpacked)
9
+
10
+ 1. Open `chrome://extensions`.
11
+ 2. Enable **Developer mode** (top right).
12
+ 3. **Load unpacked** → select this `extension/` directory.
13
+ 4. Confirm `pi-browser-use` appears and stays enabled.
14
+
15
+ The broker polls Pi's loopback tab bridge (`http://127.0.0.1:31973` by
16
+ default, override via `tabBridgePort` in `pi-browser-use` settings plus the
17
+ extension's stored `piBridgeUrl`) for open-tab requests, creates each tab
18
+ inactive in the `pi-browser-use` group, keeps the group collapsed, and
19
+ reports back per-token so Pi selects the exact tab — never by URL matching.
20
+
21
+ ## Permissions
22
+
23
+ - `tabs` / `tabGroups` — create inactive tabs, query the group by title,
24
+ enforce `collapsed: true`.
25
+ - `storage` — optional `piBridgeUrl` override.
26
+ - Loopback host access — bridge polling only; no remote hosts.
27
+
28
+ ## Notes
29
+
30
+ - Group IDs are session-scoped: the broker always queries by title and
31
+ recreates the group when absent.
32
+ - The broker never focuses windows or activates tabs; only an explicit
33
+ `bringToFront` from Pi (user-requested view / auth handoff) does that.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Pi Existing-mode tab broker (spec sections 14-18).
3
+ *
4
+ * All Pi-created tabs in the user's own Chrome must:
5
+ * 1. open inactive (active: false — never focus the browser),
6
+ * 2. live in a normal existing window (never a popup/devtools window),
7
+ * 3. enter the group named exactly "pi-browser-use",
8
+ * 4. leave that group collapsed,
9
+ * 5. never activate or focus any window as part of lookup/creation.
10
+ *
11
+ * Group IDs are valid for the current browser session only, so the group is
12
+ * always queried by title and recreated when absent — never persisted.
13
+ */
14
+
15
+ const PI_GROUP_TITLE = 'pi-browser-use'
16
+
17
+ /**
18
+ * Prefer the user's last-focused normal window without focusing anything.
19
+ * Window lookup is read-only: never mark a window focused during lookup.
20
+ */
21
+ async function getTargetNormalWindow() {
22
+ const windows = await chrome.windows.getAll({ populate: false, windowTypes: ['normal'] })
23
+ // getAll does not guarantee focus order; getLastFocused is the closest
24
+ // signal, verified to be a normal window before use.
25
+ let lastFocused = null
26
+ try {
27
+ lastFocused = await chrome.windows.getLastFocused({ populate: false })
28
+ } catch {
29
+ lastFocused = null
30
+ }
31
+ if (lastFocused && lastFocused.type === 'normal' && lastFocused.id !== undefined) {
32
+ return lastFocused
33
+ }
34
+ const candidate = windows.find((w) => w.id !== undefined)
35
+ if (candidate) return candidate
36
+ // No normal window exists: create one. The new window keeps focus wherever
37
+ // the OS puts it; we never move focus afterwards.
38
+ return chrome.windows.create({ focused: false, type: 'normal' })
39
+ }
40
+
41
+ /**
42
+ * Create one Pi tab: inactive, grouped, group collapsed afterwards so the
43
+ * collapse state is deterministic even if the user expanded the group.
44
+ */
45
+ async function openPiTab(url) {
46
+ const targetWindow = await getTargetNormalWindow()
47
+
48
+ const tab = await chrome.tabs.create({
49
+ windowId: targetWindow.id,
50
+ url,
51
+ active: false,
52
+ })
53
+
54
+ if (tab.id == null) {
55
+ throw new Error('Chrome did not return a tab ID')
56
+ }
57
+
58
+ const groups = await chrome.tabGroups.query({
59
+ title: PI_GROUP_TITLE,
60
+ windowId: targetWindow.id,
61
+ })
62
+
63
+ let groupId
64
+ if (groups.length > 0) {
65
+ groupId = groups[0].id
66
+ await chrome.tabs.group({ groupId, tabIds: [tab.id] })
67
+ } else {
68
+ groupId = await chrome.tabs.group({
69
+ tabIds: [tab.id],
70
+ createProperties: { windowId: targetWindow.id },
71
+ })
72
+ }
73
+
74
+ await chrome.tabGroups.update(groupId, {
75
+ title: PI_GROUP_TITLE,
76
+ collapsed: true,
77
+ })
78
+
79
+ return tab
80
+ }
81
+
82
+ /**
83
+ * Message contract for the ExistingSession handoff (spec section 18):
84
+ * { type: 'pi-open-tab', url, token? } -> { ok: true, tabId, windowId, token? }
85
+ * The optional token lets Pi correlate the exact target without relying on
86
+ * "first tab whose URL matches" (duplicate URLs are common).
87
+ */
88
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
89
+ if (!message || message.type !== 'pi-open-tab' || typeof message.url !== 'string') {
90
+ return false
91
+ }
92
+ openPiTab(message.url)
93
+ .then((tab) =>
94
+ sendResponse({
95
+ ok: true,
96
+ tabId: tab.id,
97
+ windowId: tab.windowId,
98
+ token: message.token ?? null,
99
+ })
100
+ )
101
+ .catch((error) =>
102
+ sendResponse({ ok: false, error: String(error && error.message ? error.message : error) })
103
+ )
104
+ // Poll now: a direct message means Pi is waiting on the bridge too.
105
+ void pollBridgeOnce().catch(() => {})
106
+ return true
107
+ })
108
+
109
+ /**
110
+ * Loopback bridge poller: Pi's tab-bridge server queues open-tab requests
111
+ * at GET /v1/pending and accepts per-token reports at POST /v1/complete.
112
+ * The bridge URL override lives in local storage (default loopback port).
113
+ */
114
+ const PI_BRIDGE_DEFAULT_URL = 'http://127.0.0.1:31973'
115
+ const PI_BRIDGE_POLL_MS = 2000
116
+ let piBridgeUrlOverride = null
117
+
118
+ try {
119
+ chrome.storage?.local.get('piBridgeUrl', (stored) => {
120
+ if (stored && typeof stored.piBridgeUrl === 'string' && stored.piBridgeUrl.length > 0) {
121
+ piBridgeUrlOverride = stored.piBridgeUrl
122
+ }
123
+ })
124
+ } catch {
125
+ // Storage unavailable: the default loopback URL still applies.
126
+ }
127
+
128
+ async function pollBridgeOnce() {
129
+ const base = piBridgeUrlOverride ?? PI_BRIDGE_DEFAULT_URL
130
+ let pending
131
+ try {
132
+ const response = await fetch(`${base}/v1/pending`)
133
+ if (!response.ok) return
134
+ pending = await response.json()
135
+ } catch {
136
+ return // Bridge not running: stay quiet until the next poll.
137
+ }
138
+ const requests = Array.isArray(pending?.requests) ? pending.requests : []
139
+ for (const request of requests) {
140
+ if (!request || typeof request.url !== 'string' || typeof request.token !== 'string') continue
141
+ try {
142
+ const tab = await openPiTab(request.url)
143
+ await fetch(`${base}/v1/complete`, {
144
+ method: 'POST',
145
+ headers: { 'content-type': 'application/json' },
146
+ body: JSON.stringify({ token: request.token, tabId: tab.id, windowId: tab.windowId }),
147
+ })
148
+ } catch (error) {
149
+ try {
150
+ await fetch(`${base}/v1/complete`, {
151
+ method: 'POST',
152
+ headers: { 'content-type': 'application/json' },
153
+ body: JSON.stringify({
154
+ token: request.token,
155
+ error: String(error && error.message ? error.message : error),
156
+ }),
157
+ })
158
+ } catch {
159
+ // The completion report is best-effort; Pi times out and fails
160
+ // clearly rather than opening an unmanaged tab.
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ setInterval(() => {
167
+ void pollBridgeOnce().catch(() => {})
168
+ }, PI_BRIDGE_POLL_MS)
169
+
170
+ /**
171
+ * MV3 service workers are short-lived: setInterval dies permanently once the
172
+ * worker is evicted, so the fast poll above only covers the awake case. The
173
+ * repeating alarm wakes a suspended worker and guarantees a poll at least
174
+ * every minute (safe floor for the alarm cadence). Re-created on every
175
+ * startup since alarm persistence across restarts is not guaranteed.
176
+ */
177
+ const PI_BRIDGE_ALARM = 'pi-bridge-poll'
178
+ try {
179
+ chrome.alarms?.create(PI_BRIDGE_ALARM, { periodInMinutes: 1 })
180
+ chrome.alarms?.onAlarm.addListener((alarm) => {
181
+ if (alarm && alarm.name === PI_BRIDGE_ALARM) void pollBridgeOnce().catch(() => {})
182
+ })
183
+ } catch {
184
+ // Alarms unavailable: the interval poll remains the only trigger.
185
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Pi Browser Use — Existing-mode tab broker",
4
+ "version": "0.2.0",
5
+ "description": "Brokers Pi-created tabs into the collapsed pi-browser-use group without stealing focus.",
6
+ "permissions": ["tabs", "tabGroups", "storage", "alarms"],
7
+ "host_permissions": ["http://127.0.0.1/*"],
8
+ "background": {
9
+ "service_worker": "background.js"
10
+ },
11
+ "minimum_chrome_version": "89"
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-use",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Opinionated browser-use for the Pi coding agent, powered by chrome-devtools-mcp (not Playwright). Fresh headless by default, authenticated persistent profile opt-in, CLI-first policy bundled.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "files": [
41
41
  "dist",
42
+ "extension",
42
43
  "skills",
43
44
  "README.md"
44
45
  ],
@@ -29,6 +29,19 @@ cp -R ~/Library/Application\ Support/Google/Chrome/Default ~/.pi/browser-profile
29
29
 
30
30
  Same macOS user, so Keychain-bound cookies and passwords decrypt fine. Prefer the headed-once flow unless you have dozens of logins — clones carry sync state, extensions, and version skew that cause strange breakage. Never copy while Chrome runs; you'll corrupt both ends.
31
31
 
32
+ ## Google SSO says "browser or app may not be secure"
33
+
34
+ Expected: Google rejects sign-in from automation-driven Chrome
35
+ (`--enable-automation`, debugging pipe, fresh profile with no history).
36
+ Don't fight it — use one of these instead:
37
+
38
+ 1. **Email + password** on the login form (no Google involved).
39
+ 2. **Your daily browser**, which Google already trusts (real history, no
40
+ automation flags). Point the session at it temporarily with
41
+ `mode: existing` and drive the flow there, then switch back.
42
+ 3. Complete SSO there once; the persistent profile keeps the resulting
43
+ Cloudflare session cookies either way.
44
+
32
45
  ## What not to do
33
46
 
34
47
  - **Paste passwords or TOTP codes into chat.** The agent never needs them — type directly in the headed window.
@@ -15,10 +15,10 @@ description: "Browser-use policy for Pi agents. Use before any browser_* tool ca
15
15
 
16
16
  ## Session modes
17
17
 
18
- - **Default: fresh headless** (`mode: fresh`). Ephemeral profile, no window, never steals focus. Use for public pages and smoke checks.
19
- - **Authenticated profile** (`mode: persistent`). Log in once in `~/.pi/browser-profile`; cookies persist. Use when the task needs your identity (private repos, Cloudflare dashboard). Both modes default headlesspass `headed: true` to watch, and warn the user before any headed launch.
20
- - **Existing Chrome** (`mode: existing`) is intrusive (drives the user's daily browser, sees all tabs). Avoid unless the user explicitly asks.
21
- - **Switch, don't restart**: `browser_switch_mode` moves between fresh and persistent mid-session. Start fresh; escalate to persistent on login walls; drop back to fresh for clean-room checks.
18
+ - **Default: persistent headless** (`mode: persistent`). Pi's own browser on its dedicated profile (`~/.pi/browser-profile`): self-launched Chrome, no window, never steals focus, no consent popups. Log in once via `browser_setup`; cookies persist. Use for everything unless there's a reason not to. Pass `headed: true` to watch, and warn the user before any headed launch.
19
+ - **Clean room** (`mode: fresh`). Ephemeral profile, thrown away each session. Use for anonymous checks, hostile links, and "does it render logged-out?" verificationsnever for anything needing identity.
20
+ - **Existing Chrome** (`mode: existing`) attaches to the user's running Chrome — intrusive (drives the daily browser, sees all tabs). Avoid unless the user explicitly asks. First attach shows Chrome's "Allow remote debugging?" consent popup (once per session — click Allow). Pi tabs must be opened with `browser_open_background_tab` (extension-brokered into the collapsed `pi-browser-use` group), never raw `browser_new_page`. Closing the last Pi tab dissolves the group automatically; `browser_close_page` refuses tabs Pi didn't open unless `force: true` was explicitly requested.
21
+ - **Switch, don't restart**: `browser_switch_mode` moves between persistent, fresh, and existing mid-session. Start persistent; drop to fresh for clean-room checks; touch existing only when the user explicitly asks.
22
22
  - **Hard blocks escalate themselves**: login walls in fresh sessions suggest the switch call; login walls and bot challenges in authenticated sessions rebuild headed and prompt the human. Once per call, never looping, never in attached sessions — and a headed popup from a block is the one case where stealing focus is the job, not a bug.
23
23
  - **Visual analysis** (`browser_analyze_screenshot`, only when `visionModel` is configured) is for canvas/WebGL scenes and coordinate clicks the tree cannot describe — not a substitute for reading the snapshot first.
24
24
 
@@ -28,8 +28,33 @@ description: "Browser-use policy for Pi agents. Use before any browser_* tool ca
28
28
 
29
29
  Turnstile, device checks, SSO/2FA cannot be automated away. On hitting one: stop, report which profile is parked where, and ask the human to solve it once in that profile. Never loop retries against a challenge page.
30
30
 
31
+ - First run: `browser_setup` opens the plain setup window; the human signs in and closes it.
32
+ - Expired session: `browser_reauth` shuts headless Chrome down cleanly, opens headed verification, then resumes headless. `variant: plain` is the maximum-compatibility fallback for providers that reject instrumented browsers.
33
+ - Site auth checks live in per-site skills (`gmail-auth` for Gmail); the browser layer only runs the headed/headless transitions.
34
+
35
+ ## Status and diagnostics
36
+
37
+ - `browser_status`: plain-language state (profile readiness, execution mode, next step). Check it before assuming auth works — bootstrap initializes the profile, it never proves a site session.
38
+ - `browser_doctor`: technical diagnostics (backend ownership, profile health, tab-bridge URL). Run it first when tools misbehave.
39
+
31
40
  ## Safety
32
41
 
33
42
  - Mutating actions (save, deploy, merge, delete) need explicit user approval.
34
43
  - Prefer `allowedUrlPattern` to cage the session to the task domains.
35
44
  - `redactNetworkHeaders` stays on; never paste secrets into pages.
45
+
46
+ ## Browser mode rules
47
+
48
+ 1. Prefer Persistent (the default) for everything: Pi's browser, invisible, no popups.
49
+ 2. Use Fresh only for anonymous/stateless browsing: hostile links, logged-out checks, clean-room reproductions.
50
+ 3. Persistent uses Pi's dedicated browser profile — never the user's daily Chrome data.
51
+ 4. If Persistent has never been initialized, launch the Pi Browser setup flow (headed once, human signs in, close the window).
52
+ 5. Never attempt to automate credentials, CAPTCHA, 2FA, passkeys, or security challenges that require the user.
53
+ 6. When authentication is required, request the headed authentication flow.
54
+ 7. After authentication, prefer restarting Persistent headless.
55
+ 8. If a site fails specifically because it is headless, retry using Persistent headed-background (per-origin; never downgrade every site).
56
+ 9. In headed-background mode, never request foreground focus unless the user explicitly asked to watch or Pi is handing over auth.
57
+ 10. Use Existing only when the user explicitly chose it or Persistent cannot provide the required existing browser/session state.
58
+ 11. In Existing mode, all new Pi tabs must be created through the Pi extension and placed in the collapsed `pi-browser-use` group.
59
+ 12. Never activate Pi-created Existing-mode tabs by default.
60
+ 13. Never close or modify unrelated user tabs; on session end close only Pi-owned tabs.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: gmail-auth
3
+ description: "Verify Gmail authentication in the persistent Pi browser profile. Use when a task needs the Gmail inbox, after browser_setup, or when Google shows a login or verification challenge."
4
+ ---
5
+
6
+ # Gmail Auth
7
+
8
+ Authentication state lives in this skill, not in a generic browser heuristic: only Gmail's inbox DOM proves the `mail.google.com` session is live. A signed-in Chrome profile and an authenticated Gmail session are different states — never conflate them.
9
+
10
+ ## Verify first
11
+
12
+ ```text
13
+ browser_open_background_tab({ "url": "https://mail.google.com/" })
14
+ browser_take_snapshot({ "pageId": <id> })
15
+ ```
16
+
17
+ Or in persistent mode, navigate directly — the persistent profile is Pi's browser, no tab group needed.
18
+
19
+ ## Authenticated
20
+
21
+ Inbox markers: `Inbox`, `Primary`, `Compose`, `Search mail`, conversation rows on a `mail.google.com` URL. Proceed with the task.
22
+
23
+ ## Login or challenge
24
+
25
+ `accounts.google.com` URL (`signin`, `ServiceLogin`, `challenge`, `password`, `otp`, `verification`) or challenge copy (`Verify it's you`, `2-Step Verification`, `Enter your password`, `Choose an account to continue`) means human auth is required:
26
+
27
+ ```text
28
+ browser_reauth({ "url": "https://mail.google.com/" })
29
+ ```
30
+
31
+ Complete verification in the headed window, then resume headless. Never paste passwords, TOTP codes, or passkeys into chat — type directly in the window.
32
+
33
+ If the provider rejects the instrumented window ("browser or app may not be secure"), retry with the maximum-compatibility variant:
34
+
35
+ ```text
36
+ browser_reauth({ "url": "https://mail.google.com/", "variant": "plain" })
37
+ ```
38
+
39
+ ## Headless vs headed
40
+
41
+ If Gmail works headed but not headless on the same profile, pin the fallback for this site only:
42
+
43
+ ```text
44
+ browser_switch_mode({ "mode": "persistent", "headed": true, "rememberSite": true })
45
+ ```
46
+
47
+ Never downgrade every site because one site rejects headless.
48
+
49
+ ## First run
50
+
51
+ Empty profile (`browser_status` says `Setup required`): run `browser_setup` once, sign in, close the window. Bootstrap initializes the profile; this skill's inbox check is still what proves Gmail works.