pi-browser-use 0.10.1 → 0.11.1
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/dist/annotate.d.ts +0 -14
- package/dist/annotate.js +0 -14
- package/dist/artifacts.d.ts +0 -2
- package/dist/artifacts.js +0 -2
- package/dist/auth-verifiers.d.ts +0 -29
- package/dist/auth-verifiers.js +0 -31
- package/dist/chrome-launcher.d.ts +0 -59
- package/dist/chrome-launcher.js +1 -55
- package/dist/client.d.ts +0 -4
- package/dist/client.js +0 -17
- package/dist/config.d.ts +0 -25
- package/dist/config.js +0 -23
- package/dist/doctor.d.ts +0 -8
- package/dist/doctor.js +0 -5
- package/dist/existing-flow.d.ts +0 -31
- package/dist/existing-flow.js +0 -29
- package/dist/focus-policy.d.ts +0 -13
- package/dist/focus-policy.js +0 -13
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +0 -10
- package/dist/named-profile.d.ts +0 -36
- package/dist/named-profile.js +0 -37
- package/dist/persistent-backend.d.ts +2 -56
- package/dist/persistent-backend.js +30 -65
- package/dist/persistent-store.d.ts +0 -24
- package/dist/persistent-store.js +0 -24
- package/dist/profile-lock.d.ts +0 -21
- package/dist/profile-lock.js +0 -27
- package/dist/profile.d.ts +0 -10
- package/dist/profile.js +0 -14
- package/dist/runtime.d.ts +0 -3
- package/dist/runtime.js +3 -92
- package/dist/session-manager.d.ts +0 -48
- package/dist/session-manager.js +0 -46
- package/dist/session.d.ts +0 -42
- package/dist/session.js +0 -18
- package/dist/settings.d.ts +0 -5
- package/dist/settings.js +0 -5
- package/dist/setup-flow.d.ts +0 -41
- package/dist/setup-flow.js +0 -37
- package/dist/shared-backend.d.ts +0 -30
- package/dist/shared-backend.js +0 -31
- package/dist/tab-bridge.d.ts +0 -27
- package/dist/tab-bridge.js +0 -27
- package/dist/tool-augment.d.ts +0 -9
- package/dist/tool-augment.js +0 -14
- package/dist/vision.d.ts +0 -17
- package/dist/vision.js +0 -17
- package/docs/performance.md +14 -0
- package/extension/README.md +2 -0
- package/extension/background.js +4 -9
- package/package.json +4 -2
- package/plugin.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -19,12 +19,10 @@ import { runBootstrap, runReauth } from './setup-flow.js';
|
|
|
19
19
|
import { normalizeOrigin, rememberExecutionPreference, resolveExecutionForOrigin, } from './session-manager.js';
|
|
20
20
|
import { DEFAULT_BRIDGE_PORT, TabBridge } from './tab-bridge.js';
|
|
21
21
|
import { handleAnalyzeScreenshot } from './vision.js';
|
|
22
|
-
// All upstream tools are re-exported with this prefix to avoid name collisions.
|
|
23
22
|
const TOOL_PREFIX = 'browser_';
|
|
24
23
|
async function callUpstream(client, name, params, signal) {
|
|
25
24
|
return (await client.callTool(name, params, signal));
|
|
26
25
|
}
|
|
27
|
-
// Noisy, slow, or privileged upstream tools; skipped during registration.
|
|
28
26
|
const EXCLUDED_TOOLS = new Set([
|
|
29
27
|
'lighthouse_audit',
|
|
30
28
|
'performance_analyze_insight',
|
|
@@ -74,12 +72,10 @@ function toToolContent(result, originalName) {
|
|
|
74
72
|
content.push({ type: 'text', text: '' });
|
|
75
73
|
return result.isError ? { content, isError: true } : { content };
|
|
76
74
|
}
|
|
77
|
-
/** Shared browser lifecycle, policy, tools, and ownership. No Pi host is required. */
|
|
78
75
|
export function createBrowserRuntime(options = {}) {
|
|
79
76
|
const defaultProfileDir = options.defaultProfileDir ?? DEFAULT_PROFILE_DIR;
|
|
80
77
|
const artifactDir = options.artifactDir ?? defaultArtifactDir();
|
|
81
78
|
let config = resolveConfig(options.config, defaultProfileDir);
|
|
82
|
-
// Remember identity even when a fresh/existing backend deliberately drops userDataDir.
|
|
83
79
|
const identityProfileDir = config.userDataDir ?? defaultProfileDir;
|
|
84
80
|
if (config.sessionMode === 'isolated')
|
|
85
81
|
config.userDataDir = undefined;
|
|
@@ -106,27 +102,18 @@ export function createBrowserRuntime(options = {}) {
|
|
|
106
102
|
combined.throwIfAborted();
|
|
107
103
|
return tool.execute(params, combined, context);
|
|
108
104
|
});
|
|
109
|
-
// Serializing calls prevents mode switches/reauth racing in-flight page actions.
|
|
110
105
|
operations = result.then(ignoreResult, ignoreResult);
|
|
111
106
|
return result;
|
|
112
107
|
},
|
|
113
108
|
});
|
|
114
109
|
}
|
|
115
|
-
// Plugin-owned persistent Chrome (self-launched, MCP attached via browserUrl).
|
|
116
|
-
// Set only for persistent mode when the legacy MCP-launch path is off.
|
|
117
110
|
let ownBackend;
|
|
118
|
-
// Existing-mode tab broker bridge. Lazy; lives for the whole session.
|
|
119
111
|
let bridge;
|
|
120
|
-
// Last navigated origin: drives the per-origin headed-background fallback.
|
|
121
112
|
let lastOrigin;
|
|
122
|
-
// URLs this session opened or navigated to in Existing mode (raw + normalized): the
|
|
123
|
-
// only close_page targets allowed there without explicit force:true.
|
|
124
113
|
const ownedUrls = new Set();
|
|
125
|
-
// This extension load's agent-session identity for the shared registry.
|
|
126
114
|
const sessionId = newSessionId();
|
|
127
115
|
const myOwner = { sessionId, pid: process.pid };
|
|
128
116
|
const shortSession = sessionId.slice(0, 8);
|
|
129
|
-
/** Registry home: the persistent profile, or the default home for existing. */
|
|
130
117
|
function registryDir() {
|
|
131
118
|
return currentMode === 'persistent' ? persistentProfileDir(config ?? {}) : defaultProfileDir;
|
|
132
119
|
}
|
|
@@ -134,8 +121,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
134
121
|
ownedUrls.add(url);
|
|
135
122
|
ownedUrls.add(normalizeTabUrl(url));
|
|
136
123
|
}
|
|
137
|
-
// Tracks which identity the live backend holds, so results can suggest
|
|
138
|
-
// escalation. 'custom' covers user-configured attach setups we did not pick.
|
|
139
124
|
let currentMode = describeMode();
|
|
140
125
|
function describeMode() {
|
|
141
126
|
if (config.sessionMode === 'existing')
|
|
@@ -153,7 +138,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
153
138
|
function persistentProfileDir(cfg) {
|
|
154
139
|
return cfg.userDataDir ?? identityProfileDir;
|
|
155
140
|
}
|
|
156
|
-
/** Close the MCP transport and any Plugin-owned Chrome. The bridge survives. */
|
|
157
141
|
async function teardownBackend() {
|
|
158
142
|
backendInitialized = false;
|
|
159
143
|
if (client) {
|
|
@@ -161,7 +145,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
161
145
|
await client.close();
|
|
162
146
|
}
|
|
163
147
|
catch {
|
|
164
|
-
// A half-dead transport must not block the switch.
|
|
165
148
|
}
|
|
166
149
|
client = undefined;
|
|
167
150
|
}
|
|
@@ -170,8 +153,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
170
153
|
await ownBackend.stop();
|
|
171
154
|
}
|
|
172
155
|
catch {
|
|
173
|
-
// Shutdown is best-effort; the profile lock release inside never
|
|
174
|
-
// throws fatally, so a new backend can still start.
|
|
175
156
|
}
|
|
176
157
|
ownBackend = undefined;
|
|
177
158
|
}
|
|
@@ -183,14 +164,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
183
164
|
saveSitePreferences(profileDir, prefs);
|
|
184
165
|
return true;
|
|
185
166
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Rebuild the backend for a mode switch. Shared by the switch tool and
|
|
188
|
-
* automatic escalation so both paths behave identically. Persistent
|
|
189
|
-
* self-launches Plugin-owned Chrome (MCP attaches via browserUrl) unless
|
|
190
|
-
* PI_BROWSER_USE_LEGACY_PERSISTENT=1. A per-origin headed-background pin
|
|
191
|
-
* wins over a headless request so one headless-hostile site never
|
|
192
|
-
* downgrades every site.
|
|
193
|
-
*/
|
|
194
167
|
async function switchBackend(mode, headed, signal, opts) {
|
|
195
168
|
const next = resolveConfig(resolveModeTarget(config, mode, headed, identityProfileDir), defaultProfileDir);
|
|
196
169
|
await teardownBackend();
|
|
@@ -221,8 +194,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
221
194
|
client = createClient(next);
|
|
222
195
|
}
|
|
223
196
|
await client.ensureReady(signal);
|
|
224
|
-
// Record what actually launched (a per-origin pin may have upgraded
|
|
225
|
-
// headless to headed-background) so status/doctor tell the truth.
|
|
226
197
|
next.headless = !effectiveHeaded;
|
|
227
198
|
config = next;
|
|
228
199
|
currentMode = mode;
|
|
@@ -234,7 +205,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
234
205
|
throw error;
|
|
235
206
|
}
|
|
236
207
|
}
|
|
237
|
-
/** Start the Existing-mode tab broker bridge on demand. */
|
|
238
208
|
async function ensureBridge() {
|
|
239
209
|
if (bridge)
|
|
240
210
|
return bridge;
|
|
@@ -252,17 +222,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
252
222
|
return '';
|
|
253
223
|
return '\n\nHint: this looks like a login wall in a fresh (logged-out) session. If the page needs your identity, call browser_switch_mode({"mode": "persistent"}) — a human must complete any SSO, 2FA, or passkey step, ideally headed.';
|
|
254
224
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Automatic escalation for hard blocks the agent cannot clear alone.
|
|
257
|
-
* Returns prompt text for the agent to relay, or empty when nothing
|
|
258
|
-
* applies. Escalates at most once per call; never loops, never retries
|
|
259
|
-
* a challenge page, and never switches away from an attached session.
|
|
260
|
-
*
|
|
261
|
-
* Challenges only escalate with navigation context: a stale "Just a
|
|
262
|
-
* moment..." shortcut tile on a New Tab snapshot must not rebuild the
|
|
263
|
-
* backend, and a challenge on another origin than requested means the
|
|
264
|
-
* navigation never landed there.
|
|
265
|
-
*/
|
|
266
225
|
async function escalateBlockedPage(url, text, signal, context) {
|
|
267
226
|
const state = classifyPageState(url, text);
|
|
268
227
|
if (state === 'ok')
|
|
@@ -271,8 +230,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
271
230
|
if (context?.tool !== 'navigate_page')
|
|
272
231
|
return '';
|
|
273
232
|
if (context.requestedUrl && url && !sameOrigin(context.requestedUrl, url)) {
|
|
274
|
-
// Challenge-provider handoffs (dedicated challenge domains) still
|
|
275
|
-
// count; anything else means the navigation never landed there.
|
|
276
233
|
const host = (() => {
|
|
277
234
|
try {
|
|
278
235
|
return new URL(url).hostname;
|
|
@@ -296,8 +253,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
296
253
|
}
|
|
297
254
|
return await escalateToHeaded(url ?? 'this page', signal);
|
|
298
255
|
}
|
|
299
|
-
// Challenge (bot check): identity never helps; only a human-gated
|
|
300
|
-
// headed window can clear it. Stay in the same mode.
|
|
301
256
|
if (config?.headless === false) {
|
|
302
257
|
return '\n\nA bot challenge is blocking this page and the browser is already visible. Complete the challenge in the window, then retry.';
|
|
303
258
|
}
|
|
@@ -306,13 +261,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
306
261
|
}
|
|
307
262
|
return await escalateToHeaded(url ?? 'this page', signal);
|
|
308
263
|
}
|
|
309
|
-
/**
|
|
310
|
-
* Rebuild the current backend headed so a human can act (log in, clear
|
|
311
|
-
* a challenge), then tell the agent exactly what to relay. Attach
|
|
312
|
-
* sessions are already visible and owned by the user: prompt only. The
|
|
313
|
-
* headed window is navigated to the blocked page and fronted: auth
|
|
314
|
-
* handoff is the one case where taking foreground is the job, not a bug.
|
|
315
|
-
*/
|
|
316
264
|
async function escalateToHeaded(url, signal) {
|
|
317
265
|
const mode = currentMode === 'persistent' ? 'persistent' : 'fresh';
|
|
318
266
|
try {
|
|
@@ -322,16 +270,12 @@ export function createBrowserRuntime(options = {}) {
|
|
|
322
270
|
return `\n\nBlocked on ${url} and the headed browser failed to launch (${error instanceof Error ? error.message : String(error)}). Ask the user to proceed manually.`;
|
|
323
271
|
}
|
|
324
272
|
if (/^https?:\/\//.test(url)) {
|
|
325
|
-
// Best effort: the window is already open for manual navigation.
|
|
326
273
|
try {
|
|
327
274
|
await callUpstream(client, 'new_page', { url, background: false }, signal);
|
|
328
275
|
}
|
|
329
276
|
catch {
|
|
330
|
-
// Manual navigation in the opened window covers this.
|
|
331
277
|
}
|
|
332
278
|
}
|
|
333
|
-
// Front Plugin-owned Chrome so the handoff window is actually visible.
|
|
334
|
-
// Fresh MCP-launched Chrome fronts itself; only Plugin-owned needs help.
|
|
335
279
|
if (ownBackend)
|
|
336
280
|
frontProcessByPid(ownBackend.pid());
|
|
337
281
|
return `\n\nBlocked on ${url}: a browser window just opened on that page (same ${mode} session — previous tabs are gone, re-list pages after). Please complete the login or challenge in that window, then tell the agent to continue. Do not close the window until done.`;
|
|
@@ -339,6 +283,9 @@ export function createBrowserRuntime(options = {}) {
|
|
|
339
283
|
async function ensureConnected(signal) {
|
|
340
284
|
signal?.throwIfAborted();
|
|
341
285
|
try {
|
|
286
|
+
if (backendInitialized && ownBackend && !ownBackend.running()) {
|
|
287
|
+
await teardownBackend();
|
|
288
|
+
}
|
|
342
289
|
if (!backendInitialized) {
|
|
343
290
|
await client?.close();
|
|
344
291
|
if (currentMode === 'persistent' && shouldSelfLaunch(config)) {
|
|
@@ -366,8 +313,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
366
313
|
const prefixedName = `${TOOL_PREFIX}${tool.name}`;
|
|
367
314
|
const originalName = tool.name;
|
|
368
315
|
const description = augmentToolDescription(prefixedName, tool.description ?? '');
|
|
369
|
-
// close_page carries an extra force gate so Existing mode can refuse
|
|
370
|
-
// to close tabs this session did not open (spec 19); stripped before upstream.
|
|
371
316
|
const parameters = originalName === 'close_page'
|
|
372
317
|
? Type.Object({
|
|
373
318
|
pageId: Type.Number({
|
|
@@ -386,9 +331,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
386
331
|
async execute(params, signal) {
|
|
387
332
|
await ensureConnected(signal);
|
|
388
333
|
const browser = client;
|
|
389
|
-
// Focus policy (headed-background / existing): Agent-created pages
|
|
390
|
-
// open in the background and selections never take foreground
|
|
391
|
-
// unless the caller explicitly asked. Explicit values always win.
|
|
392
334
|
const effectiveParams = originalName === 'new_page'
|
|
393
335
|
? applyNewPageDefaults(params)
|
|
394
336
|
: originalName === 'select_page'
|
|
@@ -396,15 +338,9 @@ export function createBrowserRuntime(options = {}) {
|
|
|
396
338
|
: params;
|
|
397
339
|
if ((originalName === 'navigate_page' || originalName === 'new_page') &&
|
|
398
340
|
typeof effectiveParams.url === 'string') {
|
|
399
|
-
// Remember the origin for the per-origin headed-background
|
|
400
|
-
// fallback; normalizeOrigin never throws (falls back to raw).
|
|
401
341
|
lastOrigin = normalizeOrigin(effectiveParams.url);
|
|
402
|
-
// In Existing mode an agent-driven navigation marks the destination
|
|
403
|
-
// as agent-touched for the close guard below.
|
|
404
342
|
if (currentMode === 'existing' && originalName === 'navigate_page')
|
|
405
343
|
trackUrl(effectiveParams.url);
|
|
406
|
-
// Claim navigated pages for this session so peer agents in a
|
|
407
|
-
// shared browser can tell ours apart (best effort, never fatal).
|
|
408
344
|
if (originalName === 'navigate_page' &&
|
|
409
345
|
(currentMode === 'persistent' || currentMode === 'existing') &&
|
|
410
346
|
typeof effectiveParams.pageId === 'number') {
|
|
@@ -412,13 +348,9 @@ export function createBrowserRuntime(options = {}) {
|
|
|
412
348
|
claimPage(registryDir(), { pageId: effectiveParams.pageId, url: effectiveParams.url }, myOwner);
|
|
413
349
|
}
|
|
414
350
|
catch {
|
|
415
|
-
// Ownership is coordination metadata, not the task itself.
|
|
416
351
|
}
|
|
417
352
|
}
|
|
418
353
|
}
|
|
419
|
-
// Never close another session's tabs: Existing mode checks Pi URL
|
|
420
|
-
// ownership (spec 19); a shared persistent backend checks the page
|
|
421
|
-
// registry instead. Both yield to explicit force:true.
|
|
422
354
|
if (originalName === 'close_page') {
|
|
423
355
|
const { force: _force, ...closeArgs } = effectiveParams;
|
|
424
356
|
void _force;
|
|
@@ -448,13 +380,10 @@ export function createBrowserRuntime(options = {}) {
|
|
|
448
380
|
releasePages(registryDir(), { pageId: effectiveParams.pageId });
|
|
449
381
|
}
|
|
450
382
|
catch {
|
|
451
|
-
// Registry hygiene never fails the close itself.
|
|
452
383
|
}
|
|
453
384
|
}
|
|
454
385
|
return { ...toToolContent(result, originalName) };
|
|
455
386
|
}
|
|
456
|
-
// Correlate a newly opened page by ID, never by URL: other agents
|
|
457
|
-
// may have the same URL open. Ambiguous results are left unclaimed.
|
|
458
387
|
let before;
|
|
459
388
|
if (originalName === 'new_page' &&
|
|
460
389
|
(currentMode === 'persistent' || currentMode === 'existing')) {
|
|
@@ -469,9 +398,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
469
398
|
if (result.isError &&
|
|
470
399
|
OVERLAY_RECOVERABLE.has(originalName) &&
|
|
471
400
|
looksOverlayBlocked(extractTextContent(result.content))) {
|
|
472
|
-
// One recovery attempt: dismiss the overlay, then retry the
|
|
473
|
-
// original call. Any failure here falls through to the
|
|
474
|
-
// original error, which already carries a hint.
|
|
475
401
|
try {
|
|
476
402
|
const escapeArgs = typeof effectiveParams.pageId === 'number'
|
|
477
403
|
? { pageId: effectiveParams.pageId, key: 'Escape' }
|
|
@@ -480,7 +406,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
480
406
|
result = await callUpstream(browser, originalName, effectiveParams, signal);
|
|
481
407
|
}
|
|
482
408
|
catch {
|
|
483
|
-
// Fall through to the original result below.
|
|
484
409
|
}
|
|
485
410
|
}
|
|
486
411
|
if (!result.isError && before) {
|
|
@@ -493,15 +418,12 @@ export function createBrowserRuntime(options = {}) {
|
|
|
493
418
|
claimPage(registryDir(), opened, myOwner);
|
|
494
419
|
}
|
|
495
420
|
catch {
|
|
496
|
-
// Ownership is coordination metadata, not the task itself.
|
|
497
421
|
}
|
|
498
422
|
}
|
|
499
423
|
}
|
|
500
424
|
const toolContent = toToolContent(result, originalName);
|
|
501
425
|
if (!toolContent.isError &&
|
|
502
426
|
(originalName === 'navigate_page' || originalName === 'take_snapshot')) {
|
|
503
|
-
// Prefer the page's real URL from the snapshot; fall back to the
|
|
504
|
-
// requested URL only when the snapshot carries none.
|
|
505
427
|
const snapshotUrl = pageUrlFromSnapshot(extractTextContent(result.content));
|
|
506
428
|
const requestedUrl = originalName === 'navigate_page' && typeof effectiveParams.url === 'string'
|
|
507
429
|
? effectiveParams.url
|
|
@@ -591,7 +513,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
591
513
|
}, signal);
|
|
592
514
|
}
|
|
593
515
|
catch {
|
|
594
|
-
// Badges are pointer-events:none and harmless if cleanup fails.
|
|
595
516
|
}
|
|
596
517
|
}
|
|
597
518
|
}
|
|
@@ -667,7 +588,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
667
588
|
],
|
|
668
589
|
};
|
|
669
590
|
}
|
|
670
|
-
// No Chrome may hold the profile while the setup window runs.
|
|
671
591
|
await teardownBackend();
|
|
672
592
|
await withProfileLock(profileDir, () => runBootstrap({
|
|
673
593
|
profileDir,
|
|
@@ -772,7 +692,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
772
692
|
};
|
|
773
693
|
}
|
|
774
694
|
if (!ownBackend && !shouldSelfLaunch(config)) {
|
|
775
|
-
// Legacy MCP-launched persistent: headed switch is the reauth path.
|
|
776
695
|
await switchBackend('persistent', true, signal);
|
|
777
696
|
return {
|
|
778
697
|
content: [
|
|
@@ -783,7 +702,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
783
702
|
],
|
|
784
703
|
};
|
|
785
704
|
}
|
|
786
|
-
// Spec §7: close headless Chrome cleanly before any headed reauth.
|
|
787
705
|
await teardownBackend();
|
|
788
706
|
const backend = createBackend({
|
|
789
707
|
config: config ?? {},
|
|
@@ -802,7 +720,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
802
720
|
});
|
|
803
721
|
const message = variant === 'plain' ? await withProfileLock(backend.profileDir(), reauth) : await reauth();
|
|
804
722
|
if (variant === 'plain') {
|
|
805
|
-
// Plain window closed by the human: resume headless automation.
|
|
806
723
|
const attach = await backend.restart(false, signal);
|
|
807
724
|
client = createClient(attach);
|
|
808
725
|
await client.ensureReady(signal);
|
|
@@ -875,7 +792,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
875
792
|
claimPage(registryDir(), { pageId: result.pageId, url: params.url }, myOwner);
|
|
876
793
|
}
|
|
877
794
|
catch {
|
|
878
|
-
// Ownership is coordination metadata, not the task itself.
|
|
879
795
|
}
|
|
880
796
|
}
|
|
881
797
|
const selectHint = result.pageId !== undefined
|
|
@@ -947,8 +863,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
947
863
|
}
|
|
948
864
|
async function initialize() {
|
|
949
865
|
if (options.lazyBrowser) {
|
|
950
|
-
// Tool discovery is browser-free even when the chosen identity is persistent.
|
|
951
|
-
// The upstream server launches/attaches Chrome only on its first browser call.
|
|
952
866
|
client = createClient({
|
|
953
867
|
...config,
|
|
954
868
|
sessionMode: 'isolated',
|
|
@@ -991,13 +905,11 @@ export function createBrowserRuntime(options = {}) {
|
|
|
991
905
|
lifetime.abort(new Error('Browser runtime is stopped.'));
|
|
992
906
|
await Promise.allSettled([startPromise, operations]);
|
|
993
907
|
await teardownBackend();
|
|
994
|
-
// Release only this runtime's claims; never terminate a borrowed browser.
|
|
995
908
|
for (const scope of new Set([identityProfileDir, defaultProfileDir])) {
|
|
996
909
|
try {
|
|
997
910
|
releasePages(scope, { sessionId });
|
|
998
911
|
}
|
|
999
912
|
catch {
|
|
1000
|
-
// Best effort.
|
|
1001
913
|
}
|
|
1002
914
|
}
|
|
1003
915
|
if (bridge) {
|
|
@@ -1005,7 +917,6 @@ export function createBrowserRuntime(options = {}) {
|
|
|
1005
917
|
await bridge.stop();
|
|
1006
918
|
}
|
|
1007
919
|
catch {
|
|
1008
|
-
// Session teardown is best-effort.
|
|
1009
920
|
}
|
|
1010
921
|
bridge = undefined;
|
|
1011
922
|
}
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrowserSessionManager: mode-specific behavior behind one stable API
|
|
3
|
-
* (spec sections 1, 8, 21, 24, 25).
|
|
4
|
-
*
|
|
5
|
-
* ```text
|
|
6
|
-
* Pi Skills / Agent → BrowserSessionManager → Fresh | Persistent | Existing
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* - Fresh: temporary profile, headless, deleted afterwards (spec 12).
|
|
10
|
-
* - Persistent: Pi-owned profile with headless-first automation and a
|
|
11
|
-
* headed-background fallback per origin (spec 2, 4, 7, 8, 24).
|
|
12
|
-
* - Existing: the user's own Chrome via autoConnect; Pi tabs are brokered by
|
|
13
|
-
* the Pi extension into the collapsed `pi-browser-use` group (spec 13-20).
|
|
14
|
-
*
|
|
15
|
-
* This module holds the state machines, capability resolution, and fallback
|
|
16
|
-
* hierarchies. Process launching (chrome-launcher), locking (profile-lock),
|
|
17
|
-
* and durable metadata (persistent-store) are injected collaborators so the
|
|
18
|
-
* logic stays unit-testable without a real Chrome.
|
|
19
|
-
*/
|
|
20
1
|
import type { BrowserCapabilityRequest, BrowserSession, BrowserSessionMode, BrowserStatus, PageHandle, PersistentBrowserMetadata, PersistentExecutionMode, PersistentState, SiteBrowserPreference } from './session.js';
|
|
21
2
|
export type { BrowserSession, BrowserStatus, PageHandle } from './session.js';
|
|
22
|
-
/** Minimal transport SPI a session uses to drive pages (MCP/CDP/extension). */
|
|
23
3
|
export interface SessionTransport {
|
|
24
4
|
openPage(url: string): Promise<PageHandle>;
|
|
25
5
|
listPages(): Promise<PageHandle[]>;
|
|
26
6
|
closePage(page: PageHandle): Promise<void>;
|
|
27
7
|
requestVisibleBrowser?(reason: string): Promise<void>;
|
|
28
8
|
}
|
|
29
|
-
/** In-memory transport for tests and for wiring custom backends. */
|
|
30
9
|
export declare function createMemoryTransport(): SessionTransport & {
|
|
31
10
|
pages: PageHandle[];
|
|
32
11
|
};
|
|
@@ -42,20 +21,14 @@ declare abstract class BaseSession implements BrowserSession {
|
|
|
42
21
|
abstract getStatus(): Promise<BrowserStatus>;
|
|
43
22
|
shutdown(): Promise<void>;
|
|
44
23
|
}
|
|
45
|
-
/** Fresh: anonymous, stateless, headless. Never inherits auth (spec 12). */
|
|
46
24
|
export declare class FreshSession extends BaseSession {
|
|
47
25
|
constructor(transport: SessionTransport);
|
|
48
26
|
getStatus(): Promise<BrowserStatus>;
|
|
49
27
|
}
|
|
50
|
-
/** Events driving the Persistent state machine (spec section 2). */
|
|
51
28
|
export type PersistentEvent = 'bootstrap-needed' | 'bootstrap-opened' | 'bootstrap-closed' | 'automation-started' | 'automation-succeeded' | 'auth-required' | 'reauth-opened' | 'reauth-completed' | 'headless-incompatible';
|
|
52
|
-
/** Pure Persistent lifecycle transition. Returns the state unchanged for unknown events. */
|
|
53
29
|
export declare function nextPersistentState(state: PersistentState, event: PersistentEvent): PersistentState;
|
|
54
|
-
/** Normalize a URL or origin to a bare origin for preference lookup. */
|
|
55
30
|
export declare function normalizeOrigin(urlOrOrigin: string): string;
|
|
56
|
-
/** Resolve the execution mode for an origin or full URL (spec section 8). */
|
|
57
31
|
export declare function resolveExecutionForOrigin(preferences: SiteBrowserPreference[], urlOrOrigin: string, fallback?: PersistentExecutionMode): PersistentExecutionMode;
|
|
58
|
-
/** Remember/overwrite the per-origin preference; never downgrades globally. */
|
|
59
32
|
export declare function rememberExecutionPreference(preferences: SiteBrowserPreference[], urlOrOrigin: string, executionMode: PersistentExecutionMode): SiteBrowserPreference[];
|
|
60
33
|
export interface PersistentSessionOptions {
|
|
61
34
|
transport: SessionTransport;
|
|
@@ -64,10 +37,6 @@ export interface PersistentSessionOptions {
|
|
|
64
37
|
executionMode?: PersistentExecutionMode;
|
|
65
38
|
sitePreferences?: SiteBrowserPreference[];
|
|
66
39
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Persistent: Pi's browser identity. Headless first; per-origin
|
|
69
|
-
* headed-background fallback; reauth via headed browser (spec 2, 7, 8).
|
|
70
|
-
*/
|
|
71
40
|
export declare class PersistentSession extends BaseSession {
|
|
72
41
|
private persistentState;
|
|
73
42
|
private executionMode;
|
|
@@ -79,15 +48,9 @@ export declare class PersistentSession extends BaseSession {
|
|
|
79
48
|
get preferences(): SiteBrowserPreference[];
|
|
80
49
|
send(event: PersistentEvent): PersistentState;
|
|
81
50
|
executionFor(urlOrOrigin: string): PersistentExecutionMode;
|
|
82
|
-
/** A site failed headless while headed works: pin headed-background there. */
|
|
83
51
|
markHeadlessIncompatible(urlOrOrigin: string): void;
|
|
84
52
|
getStatus(): Promise<BrowserStatus>;
|
|
85
53
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Existing: the user's browser identity via autoConnect (spec 13).
|
|
88
|
-
* All Pi-created tabs must go through the extension broker into the
|
|
89
|
-
* collapsed `pi-browser-use` group — never raw foreground tabs.
|
|
90
|
-
*/
|
|
91
54
|
export declare class ExistingSession extends BaseSession {
|
|
92
55
|
constructor(transport: SessionTransport);
|
|
93
56
|
getStatus(): Promise<BrowserStatus>;
|
|
@@ -97,19 +60,9 @@ export interface ManagedSessions {
|
|
|
97
60
|
persistent: BrowserSession;
|
|
98
61
|
existing: BrowserSession;
|
|
99
62
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Resolve a capability request to a mode (spec section 21):
|
|
102
|
-
* persistence/auth → persistent; explicit existing opt-in → existing;
|
|
103
|
-
* otherwise fresh. Callers escalate along the fallback hierarchies
|
|
104
|
-
* (sections 24/25) when the resolved mode cannot serve.
|
|
105
|
-
*/
|
|
106
63
|
export declare function resolveModeForCapabilities(request: BrowserCapabilityRequest, persistentInitialized: boolean): BrowserSessionMode;
|
|
107
|
-
/** Persistent escalation sequence (spec section 24). */
|
|
108
64
|
export type PersistentEscalation = 'headless' | 'headed-auth-then-headless' | 'headed-background' | 'suggest-existing';
|
|
109
|
-
/** Next step when persistent headless cannot proceed. */
|
|
110
65
|
export declare function nextPersistentEscalation(reason: 'works' | 'login-needed' | 'headless-incompatible'): PersistentEscalation;
|
|
111
|
-
/** Existing-mode creation contract (spec section 25): fail loudly when the
|
|
112
|
-
* extension broker is unavailable rather than opening unmanaged tabs. */
|
|
113
66
|
export declare function assertExistingBrokerAvailable(available: boolean): void;
|
|
114
67
|
export declare class BrowserSessionManager {
|
|
115
68
|
private readonly sessions;
|
|
@@ -118,7 +71,6 @@ export declare class BrowserSessionManager {
|
|
|
118
71
|
getMode(): BrowserSessionMode;
|
|
119
72
|
getSession(mode?: BrowserSessionMode): BrowserSession;
|
|
120
73
|
switchTo(mode: BrowserSessionMode): Promise<BrowserSession>;
|
|
121
|
-
/** Capability-based selection (spec 21): skills ask, manager resolves. */
|
|
122
74
|
require(request: BrowserCapabilityRequest): Promise<BrowserSession>;
|
|
123
75
|
shutdownAll(): Promise<void>;
|
|
124
76
|
}
|
package/dist/session-manager.js
CHANGED
|
@@ -1,24 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrowserSessionManager: mode-specific behavior behind one stable API
|
|
3
|
-
* (spec sections 1, 8, 21, 24, 25).
|
|
4
|
-
*
|
|
5
|
-
* ```text
|
|
6
|
-
* Pi Skills / Agent → BrowserSessionManager → Fresh | Persistent | Existing
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* - Fresh: temporary profile, headless, deleted afterwards (spec 12).
|
|
10
|
-
* - Persistent: Pi-owned profile with headless-first automation and a
|
|
11
|
-
* headed-background fallback per origin (spec 2, 4, 7, 8, 24).
|
|
12
|
-
* - Existing: the user's own Chrome via autoConnect; Pi tabs are brokered by
|
|
13
|
-
* the Pi extension into the collapsed `pi-browser-use` group (spec 13-20).
|
|
14
|
-
*
|
|
15
|
-
* This module holds the state machines, capability resolution, and fallback
|
|
16
|
-
* hierarchies. Process launching (chrome-launcher), locking (profile-lock),
|
|
17
|
-
* and durable metadata (persistent-store) are injected collaborators so the
|
|
18
|
-
* logic stays unit-testable without a real Chrome.
|
|
19
|
-
*/
|
|
20
1
|
import { PI_GROUP_TITLE } from './focus-policy.js';
|
|
21
|
-
/** In-memory transport for tests and for wiring custom backends. */
|
|
22
2
|
export function createMemoryTransport() {
|
|
23
3
|
const pages = [];
|
|
24
4
|
let counter = 0;
|
|
@@ -62,7 +42,6 @@ class BaseSession {
|
|
|
62
42
|
}
|
|
63
43
|
async shutdown() { }
|
|
64
44
|
}
|
|
65
|
-
/** Fresh: anonymous, stateless, headless. Never inherits auth (spec 12). */
|
|
66
45
|
export class FreshSession extends BaseSession {
|
|
67
46
|
constructor(transport) {
|
|
68
47
|
super('fresh', transport);
|
|
@@ -93,11 +72,9 @@ const PERSISTENT_TRANSITIONS = {
|
|
|
93
72
|
REAUTH_REQUIRED: { 'reauth-opened': 'REAUTH_HEADFUL' },
|
|
94
73
|
REAUTH_HEADFUL: { 'reauth-completed': 'READY' },
|
|
95
74
|
};
|
|
96
|
-
/** Pure Persistent lifecycle transition. Returns the state unchanged for unknown events. */
|
|
97
75
|
export function nextPersistentState(state, event) {
|
|
98
76
|
return PERSISTENT_TRANSITIONS[state][event] ?? state;
|
|
99
77
|
}
|
|
100
|
-
/** Normalize a URL or origin to a bare origin for preference lookup. */
|
|
101
78
|
export function normalizeOrigin(urlOrOrigin) {
|
|
102
79
|
try {
|
|
103
80
|
return new URL(urlOrOrigin).origin;
|
|
@@ -106,23 +83,17 @@ export function normalizeOrigin(urlOrOrigin) {
|
|
|
106
83
|
return urlOrOrigin;
|
|
107
84
|
}
|
|
108
85
|
}
|
|
109
|
-
/** Resolve the execution mode for an origin or full URL (spec section 8). */
|
|
110
86
|
export function resolveExecutionForOrigin(preferences, urlOrOrigin, fallback = 'headless') {
|
|
111
87
|
const origin = normalizeOrigin(urlOrOrigin);
|
|
112
88
|
const match = preferences.find((p) => normalizeOrigin(p.origin) === origin);
|
|
113
89
|
return match?.executionMode ?? fallback;
|
|
114
90
|
}
|
|
115
|
-
/** Remember/overwrite the per-origin preference; never downgrades globally. */
|
|
116
91
|
export function rememberExecutionPreference(preferences, urlOrOrigin, executionMode) {
|
|
117
92
|
const origin = normalizeOrigin(urlOrOrigin);
|
|
118
93
|
const next = preferences.filter((p) => normalizeOrigin(p.origin) !== origin);
|
|
119
94
|
next.push({ origin, executionMode });
|
|
120
95
|
return next;
|
|
121
96
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Persistent: Pi's browser identity. Headless first; per-origin
|
|
124
|
-
* headed-background fallback; reauth via headed browser (spec 2, 7, 8).
|
|
125
|
-
*/
|
|
126
97
|
export class PersistentSession extends BaseSession {
|
|
127
98
|
persistentState;
|
|
128
99
|
executionMode;
|
|
@@ -158,14 +129,12 @@ export class PersistentSession extends BaseSession {
|
|
|
158
129
|
return this.executionMode;
|
|
159
130
|
}
|
|
160
131
|
}
|
|
161
|
-
/** A site failed headless while headed works: pin headed-background there. */
|
|
162
132
|
markHeadlessIncompatible(urlOrOrigin) {
|
|
163
133
|
try {
|
|
164
134
|
const origin = new URL(urlOrOrigin).origin;
|
|
165
135
|
this.sitePreferences = rememberExecutionPreference(this.sitePreferences, origin, 'headed-background');
|
|
166
136
|
}
|
|
167
137
|
catch {
|
|
168
|
-
// Non-URL input: fall back to the session default without pinning.
|
|
169
138
|
this.executionMode = 'headed-background';
|
|
170
139
|
}
|
|
171
140
|
this.send('headless-incompatible');
|
|
@@ -186,11 +155,6 @@ export class PersistentSession extends BaseSession {
|
|
|
186
155
|
};
|
|
187
156
|
}
|
|
188
157
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Existing: the user's browser identity via autoConnect (spec 13).
|
|
191
|
-
* All Pi-created tabs must go through the extension broker into the
|
|
192
|
-
* collapsed `pi-browser-use` group — never raw foreground tabs.
|
|
193
|
-
*/
|
|
194
158
|
export class ExistingSession extends BaseSession {
|
|
195
159
|
constructor(transport) {
|
|
196
160
|
super('existing', transport);
|
|
@@ -204,12 +168,6 @@ export class ExistingSession extends BaseSession {
|
|
|
204
168
|
};
|
|
205
169
|
}
|
|
206
170
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Resolve a capability request to a mode (spec section 21):
|
|
209
|
-
* persistence/auth → persistent; explicit existing opt-in → existing;
|
|
210
|
-
* otherwise fresh. Callers escalate along the fallback hierarchies
|
|
211
|
-
* (sections 24/25) when the resolved mode cannot serve.
|
|
212
|
-
*/
|
|
213
171
|
export function resolveModeForCapabilities(request, persistentInitialized) {
|
|
214
172
|
if (request.persistence === true || request.authentication) {
|
|
215
173
|
return 'persistent';
|
|
@@ -219,7 +177,6 @@ export function resolveModeForCapabilities(request, persistentInitialized) {
|
|
|
219
177
|
}
|
|
220
178
|
return 'fresh';
|
|
221
179
|
}
|
|
222
|
-
/** Next step when persistent headless cannot proceed. */
|
|
223
180
|
export function nextPersistentEscalation(reason) {
|
|
224
181
|
if (reason === 'works')
|
|
225
182
|
return 'headless';
|
|
@@ -227,8 +184,6 @@ export function nextPersistentEscalation(reason) {
|
|
|
227
184
|
return 'headed-auth-then-headless';
|
|
228
185
|
return 'headed-background';
|
|
229
186
|
}
|
|
230
|
-
/** Existing-mode creation contract (spec section 25): fail loudly when the
|
|
231
|
-
* extension broker is unavailable rather than opening unmanaged tabs. */
|
|
232
187
|
export function assertExistingBrokerAvailable(available) {
|
|
233
188
|
if (!available) {
|
|
234
189
|
throw new Error(`Existing-mode tab broker unavailable: refusing to open unmanaged tabs. ` +
|
|
@@ -256,7 +211,6 @@ export class BrowserSessionManager {
|
|
|
256
211
|
await session.ensureReady();
|
|
257
212
|
return session;
|
|
258
213
|
}
|
|
259
|
-
/** Capability-based selection (spec 21): skills ask, manager resolves. */
|
|
260
214
|
async require(request) {
|
|
261
215
|
const persistent = this.sessions.persistent;
|
|
262
216
|
let initialized = false;
|