pi-browser-use 0.6.1 → 0.8.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.
- package/README.md +39 -11
- package/dist/auth-verifiers.d.ts +60 -0
- package/dist/auth-verifiers.d.ts.map +1 -0
- package/dist/auth-verifiers.js +92 -0
- package/dist/auth-verifiers.js.map +1 -0
- package/dist/chrome-launcher.d.ts +100 -0
- package/dist/chrome-launcher.d.ts.map +1 -0
- package/dist/chrome-launcher.js +268 -0
- package/dist/chrome-launcher.js.map +1 -0
- package/dist/config.d.ts +3 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -5
- package/dist/config.js.map +1 -1
- package/dist/doctor.d.ts +11 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -7
- package/dist/doctor.js.map +1 -1
- package/dist/existing-flow.d.ts +68 -0
- package/dist/existing-flow.d.ts.map +1 -0
- package/dist/existing-flow.js +128 -0
- package/dist/existing-flow.js.map +1 -0
- package/dist/focus-policy.d.ts +28 -0
- package/dist/focus-policy.d.ts.map +1 -0
- package/dist/focus-policy.js +28 -0
- package/dist/focus-policy.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +565 -39
- package/dist/index.js.map +1 -1
- package/dist/named-profile.d.ts +47 -0
- package/dist/named-profile.d.ts.map +1 -0
- package/dist/named-profile.js +123 -0
- package/dist/named-profile.js.map +1 -0
- package/dist/persistent-backend.d.ts +90 -0
- package/dist/persistent-backend.d.ts.map +1 -0
- package/dist/persistent-backend.js +229 -0
- package/dist/persistent-backend.js.map +1 -0
- package/dist/persistent-store.d.ts +35 -0
- package/dist/persistent-store.d.ts.map +1 -0
- package/dist/persistent-store.js +98 -0
- package/dist/persistent-store.js.map +1 -0
- package/dist/profile-lock.d.ts +40 -0
- package/dist/profile-lock.d.ts.map +1 -0
- package/dist/profile-lock.js +165 -0
- package/dist/profile-lock.js.map +1 -0
- package/dist/profile.d.ts +3 -1
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +15 -6
- package/dist/profile.js.map +1 -1
- package/dist/session-manager.d.ts +125 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +275 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/session.d.ts +92 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +29 -0
- package/dist/session.js.map +1 -0
- package/dist/setup-flow.d.ts +78 -0
- package/dist/setup-flow.d.ts.map +1 -0
- package/dist/setup-flow.js +97 -0
- package/dist/setup-flow.js.map +1 -0
- package/dist/shared-backend.d.ts +70 -0
- package/dist/shared-backend.d.ts.map +1 -0
- package/dist/shared-backend.js +152 -0
- package/dist/shared-backend.js.map +1 -0
- package/dist/tab-bridge.d.ts +71 -0
- package/dist/tab-bridge.d.ts.map +1 -0
- package/dist/tab-bridge.js +198 -0
- package/dist/tab-bridge.js.map +1 -0
- package/extension/README.md +33 -0
- package/extension/background.js +185 -0
- package/extension/manifest.json +12 -0
- package/package.json +2 -1
- package/skills/auth-bootstrap/SKILL.md +13 -0
- package/skills/browser-policy/SKILL.md +39 -4
- package/skills/gmail-auth/SKILL.md +51 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-backend registry + per-agent page ownership (multi-agent support).
|
|
3
|
+
*
|
|
4
|
+
* Many Pi agents share one Pi-owned Chrome: the first backend writes
|
|
5
|
+
* `<profile>.backend.json` ({ pid, browserUrl, sessionId, startedAt });
|
|
6
|
+
* latecomers attach to that browserUrl instead of launching a second Chrome
|
|
7
|
+
* (the profile lock still forbids two processes — sharing is by attach).
|
|
8
|
+
*
|
|
9
|
+
* Tab separation is by ownership, not visuals: every page a Pi session opens
|
|
10
|
+
* (or Pi-navigates, in shared modes) is recorded in `<profile>.pages.json`
|
|
11
|
+
* with its owner session. Entries whose owner pid is dead are pruned
|
|
12
|
+
* lazily. The close guard refuses pages owned by another *live* session.
|
|
13
|
+
*
|
|
14
|
+
* Page-id routing (upstream `experimentalPageIdRouting`) plus explicit
|
|
15
|
+
* pageIds on every call keeps agents from driving each other's tabs; this
|
|
16
|
+
* registry adds the missing pieces: discovery, ownership, and cleanup scope.
|
|
17
|
+
*/
|
|
18
|
+
import type { McpPageEntry } from './existing-flow.js';
|
|
19
|
+
export interface BackendAdvert {
|
|
20
|
+
pid: number;
|
|
21
|
+
browserUrl: string;
|
|
22
|
+
port: number;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function backendAdvertPathFor(profileDir: string): string;
|
|
27
|
+
/** This process's agent-session identity (one per extension load). */
|
|
28
|
+
export declare function newSessionId(): string;
|
|
29
|
+
/** Publish this backend for peer agents. Overwrites stale entries. */
|
|
30
|
+
export declare function advertiseBackend(profileDir: string, advert: BackendAdvert): void;
|
|
31
|
+
/** Read a live peer advert, or undefined (absent/corrupt/dead owner). */
|
|
32
|
+
export declare function readLiveAdvert(profileDir: string): BackendAdvert | undefined;
|
|
33
|
+
/** Withdraw our advert (backend shutdown). Best effort. */
|
|
34
|
+
export declare function withdrawAdvert(profileDir: string, sessionId: string): void;
|
|
35
|
+
export interface PageOwner {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
pid: number;
|
|
38
|
+
}
|
|
39
|
+
export interface OwnedPage {
|
|
40
|
+
pageId: number;
|
|
41
|
+
url?: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
owner: PageOwner;
|
|
44
|
+
openedAt: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function pageRegistryPathFor(profileDir: string): string;
|
|
47
|
+
/** Drop entries whose owner process is gone. */
|
|
48
|
+
export declare function pruneDeadOwners(profileDir: string): OwnedPage[];
|
|
49
|
+
/** Claim a page for a session (open or Pi-navigate). */
|
|
50
|
+
export declare function claimPage(profileDir: string, page: McpPageEntry, owner: PageOwner, at?: string): void;
|
|
51
|
+
/** Release one page (closed) or all of a session's pages (shutdown). */
|
|
52
|
+
export declare function releasePages(profileDir: string, filter: {
|
|
53
|
+
pageId?: number;
|
|
54
|
+
sessionId?: string;
|
|
55
|
+
}): void;
|
|
56
|
+
/** Live sessions currently holding pages (peers to avoid disturbing). */
|
|
57
|
+
export declare function livePeerSessions(profileDir: string): PageOwner[];
|
|
58
|
+
export type CloseVerdict = {
|
|
59
|
+
ok: true;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Shared-mode close verdict: our own or unclaimed pages may close; pages
|
|
66
|
+
* owned by another live session need explicit force. Stale ids fail safe
|
|
67
|
+
* with a re-list hint (ids shift when tabs close).
|
|
68
|
+
*/
|
|
69
|
+
export declare function checkSharedCloseAllowed(entries: McpPageEntry[], pageId: number, profileDir: string, self: PageOwner, ownedUrls: ReadonlySet<string>): CloseVerdict;
|
|
70
|
+
//# sourceMappingURL=shared-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-backend.d.ts","sourceRoot":"","sources":["../src/shared-backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAWD,sEAAsE;AACtE,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAGhF;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAgB5E;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAO1E;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAwBD,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAI/D;AAED,wDAAwD;AACxD,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,SAAS,EAChB,EAAE,SAA2B,GAC5B,IAAI,CAIN;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAC1B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,IAAI,CAON;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAMhE;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvE;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,YAAY,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAC7B,YAAY,CAyBd"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-backend registry + per-agent page ownership (multi-agent support).
|
|
3
|
+
*
|
|
4
|
+
* Many Pi agents share one Pi-owned Chrome: the first backend writes
|
|
5
|
+
* `<profile>.backend.json` ({ pid, browserUrl, sessionId, startedAt });
|
|
6
|
+
* latecomers attach to that browserUrl instead of launching a second Chrome
|
|
7
|
+
* (the profile lock still forbids two processes — sharing is by attach).
|
|
8
|
+
*
|
|
9
|
+
* Tab separation is by ownership, not visuals: every page a Pi session opens
|
|
10
|
+
* (or Pi-navigates, in shared modes) is recorded in `<profile>.pages.json`
|
|
11
|
+
* with its owner session. Entries whose owner pid is dead are pruned
|
|
12
|
+
* lazily. The close guard refuses pages owned by another *live* session.
|
|
13
|
+
*
|
|
14
|
+
* Page-id routing (upstream `experimentalPageIdRouting`) plus explicit
|
|
15
|
+
* pageIds on every call keeps agents from driving each other's tabs; this
|
|
16
|
+
* registry adds the missing pieces: discovery, ownership, and cleanup scope.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
21
|
+
export function backendAdvertPathFor(profileDir) {
|
|
22
|
+
return `${profileDir}.backend.json`;
|
|
23
|
+
}
|
|
24
|
+
function isPidAlive(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** This process's agent-session identity (one per extension load). */
|
|
34
|
+
export function newSessionId() {
|
|
35
|
+
return randomUUID();
|
|
36
|
+
}
|
|
37
|
+
/** Publish this backend for peer agents. Overwrites stale entries. */
|
|
38
|
+
export function advertiseBackend(profileDir, advert) {
|
|
39
|
+
mkdirSync(dirname(backendAdvertPathFor(profileDir)), { recursive: true });
|
|
40
|
+
writeFileSync(backendAdvertPathFor(profileDir), `${JSON.stringify(advert, null, 2)}\n`, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
/** Read a live peer advert, or undefined (absent/corrupt/dead owner). */
|
|
43
|
+
export function readLiveAdvert(profileDir) {
|
|
44
|
+
try {
|
|
45
|
+
const advert = JSON.parse(readFileSync(backendAdvertPathFor(profileDir), 'utf8'));
|
|
46
|
+
if (typeof advert?.browserUrl !== 'string' ||
|
|
47
|
+
typeof advert?.pid !== 'number' ||
|
|
48
|
+
!isPidAlive(advert.pid)) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return advert;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Withdraw our advert (backend shutdown). Best effort. */
|
|
58
|
+
export function withdrawAdvert(profileDir, sessionId) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = JSON.parse(readFileSync(backendAdvertPathFor(profileDir), 'utf8'));
|
|
61
|
+
if (raw?.sessionId === sessionId)
|
|
62
|
+
rmSync(backendAdvertPathFor(profileDir), { force: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Another backend already replaced it; leave it alone.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function pageRegistryPathFor(profileDir) {
|
|
69
|
+
return `${profileDir}.pages.json`;
|
|
70
|
+
}
|
|
71
|
+
function readRegistry(profileDir) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = JSON.parse(readFileSync(pageRegistryPathFor(profileDir), 'utf8'));
|
|
74
|
+
if (!Array.isArray(raw))
|
|
75
|
+
return [];
|
|
76
|
+
return raw.filter((entry) => typeof entry === 'object' &&
|
|
77
|
+
entry !== null &&
|
|
78
|
+
typeof entry.pageId === 'number' &&
|
|
79
|
+
typeof entry.owner?.sessionId === 'string' &&
|
|
80
|
+
typeof entry.owner?.pid === 'number');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function writeRegistry(profileDir, pages) {
|
|
87
|
+
mkdirSync(dirname(pageRegistryPathFor(profileDir)), { recursive: true });
|
|
88
|
+
writeFileSync(pageRegistryPathFor(profileDir), `${JSON.stringify(pages, null, 2)}\n`, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
/** Drop entries whose owner process is gone. */
|
|
91
|
+
export function pruneDeadOwners(profileDir) {
|
|
92
|
+
const live = readRegistry(profileDir).filter((entry) => isPidAlive(entry.owner.pid));
|
|
93
|
+
writeRegistry(profileDir, live);
|
|
94
|
+
return live;
|
|
95
|
+
}
|
|
96
|
+
/** Claim a page for a session (open or Pi-navigate). */
|
|
97
|
+
export function claimPage(profileDir, page, owner, at = new Date().toISOString()) {
|
|
98
|
+
const pages = pruneDeadOwners(profileDir).filter((entry) => entry.pageId !== page.pageId);
|
|
99
|
+
pages.push({ pageId: page.pageId, url: page.url, title: page.title, owner, openedAt: at });
|
|
100
|
+
writeRegistry(profileDir, pages);
|
|
101
|
+
}
|
|
102
|
+
/** Release one page (closed) or all of a session's pages (shutdown). */
|
|
103
|
+
export function releasePages(profileDir, filter) {
|
|
104
|
+
const pages = pruneDeadOwners(profileDir).filter((entry) => {
|
|
105
|
+
if (filter.pageId !== undefined && entry.pageId === filter.pageId)
|
|
106
|
+
return false;
|
|
107
|
+
if (filter.sessionId !== undefined && entry.owner.sessionId === filter.sessionId)
|
|
108
|
+
return false;
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
writeRegistry(profileDir, pages);
|
|
112
|
+
}
|
|
113
|
+
/** Live sessions currently holding pages (peers to avoid disturbing). */
|
|
114
|
+
export function livePeerSessions(profileDir) {
|
|
115
|
+
const seen = new Map();
|
|
116
|
+
for (const entry of pruneDeadOwners(profileDir)) {
|
|
117
|
+
if (!seen.has(entry.owner.sessionId))
|
|
118
|
+
seen.set(entry.owner.sessionId, entry.owner);
|
|
119
|
+
}
|
|
120
|
+
return [...seen.values()];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Shared-mode close verdict: our own or unclaimed pages may close; pages
|
|
124
|
+
* owned by another live session need explicit force. Stale ids fail safe
|
|
125
|
+
* with a re-list hint (ids shift when tabs close).
|
|
126
|
+
*/
|
|
127
|
+
export function checkSharedCloseAllowed(entries, pageId, profileDir, self, ownedUrls) {
|
|
128
|
+
const target = entries.find((entry) => entry.pageId === pageId);
|
|
129
|
+
if (!target) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `No page ${pageId} is currently open. Re-list pages; page IDs shift when tabs close.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const claim = pruneDeadOwners(profileDir).find((entry) => entry.pageId === pageId);
|
|
136
|
+
if (claim && claim.owner.sessionId !== self.sessionId) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reason: `Refusing to close page ${pageId} (${claim.owner.sessionId.slice(0, 8)}'s tab${target.title ? ` "${target.title}"` : ''}): ` +
|
|
140
|
+
`another live agent session owns it. Pass force:true only if the user explicitly asked for this exact tab.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const url = target.url ?? '';
|
|
144
|
+
if (claim || url === '' || ownedUrls.has(url))
|
|
145
|
+
return { ok: true };
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: `Refusing to close page ${pageId} (${target.title ?? url ?? 'unknown tab'}): ` +
|
|
149
|
+
`this session did not open it. Pass force:true only if the user explicitly asked for this exact tab.`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=shared-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-backend.js","sourceRoot":"","sources":["../src/shared-backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":"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"}
|
|
@@ -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.
|