@parall/daemon 1.45.0 → 1.47.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/bundle/manifest.json +15 -15
- package/bundle/parall-browser-pod.js +29796 -379
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26161 -277
- package/bundle/parall-codex-agent.js +26720 -335
- package/bundle/parall-daemon.js +31760 -2001
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/local-control.d.ts +59 -0
- package/dist/local-control.d.ts.map +1 -0
- package/dist/local-control.js +230 -0
- package/dist/local-profile-control.d.ts +77 -0
- package/dist/local-profile-control.d.ts.map +1 -0
- package/dist/local-profile-control.js +108 -0
- package/dist/supervisor.d.ts +19 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +90 -187
- package/package.json +8 -6
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { sleep } from './subprocess.js';
|
|
2
|
+
/**
|
|
3
|
+
* BrowserTargetRegistry — binds every browser command to a page target OWNED by
|
|
4
|
+
* the profile's bb-browser account, so no command can fall through to
|
|
5
|
+
* bb-browser's account-blind global routing.
|
|
6
|
+
*
|
|
7
|
+
* WHY. bb-browser-pro 0.15.0 resolves a tab-less command via
|
|
8
|
+
* `ensurePageTarget(undefined)` → global `currentTargetId` → `targets[0]`
|
|
9
|
+
* (verified in its daemon source; `request.account` is never consulted). Chrome
|
|
10
|
+
* is launched with a literal `about:blank` positional tab in the DEFAULT
|
|
11
|
+
* browser context — no account cookies, no account proxy — and nothing ever
|
|
12
|
+
* closes it, so it IS `targets[0]` on a fresh Chromium. Worse, the daemon's own
|
|
13
|
+
* tab-less `tab_list` warm-up is dispatched through `ensurePageTarget` too and
|
|
14
|
+
* pins the global `currentTargetId` to that blank page. Any agent command sent
|
|
15
|
+
* without an explicit `tabId` (navigate/get/click/screenshot/eval/...) then
|
|
16
|
+
* executes against a cookie-less, proxy-less page — including real-IP egress
|
|
17
|
+
* for proxy-configured profiles. The registry closes this by resolving an
|
|
18
|
+
* OWNED target (per `tab_list`'s per-tab `account` attribution) and stamping
|
|
19
|
+
* its full CDP targetId on every tab-addressed command.
|
|
20
|
+
*
|
|
21
|
+
* Ownership ground truth is bb-browser's own `tab_list` attribution (targetId →
|
|
22
|
+
* account, derived from its BrowserContext bookkeeping), so a recreated tab —
|
|
23
|
+
* `tab_new {account}` routes through `createTabInContext` → `ensureContext` —
|
|
24
|
+
* always lands in the profile's BrowserContext with its cookies re-injected,
|
|
25
|
+
* never the default context.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Thrown when a command's target cannot be bound to a profile-owned page: a
|
|
29
|
+
* caller-supplied ref that is not the profile's own tab (foreign tab, numeric
|
|
30
|
+
* global index, stale id), or an owned page that cannot be (re)created. The
|
|
31
|
+
* command is NEVER retargeted at another page — fail closed.
|
|
32
|
+
*/
|
|
33
|
+
export class BrowserTargetError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
constructor(message, code) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = 'BrowserTargetError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class BrowserTargetRegistry {
|
|
42
|
+
host;
|
|
43
|
+
opts;
|
|
44
|
+
// Deliberately STATELESS: ownership is recomputed from bb-browser's live
|
|
45
|
+
// `tab_list` attribution on every resolve. There is no cached binding to go
|
|
46
|
+
// stale, nothing to invalidate on restart/reset, and a resolve can never
|
|
47
|
+
// return a tab that isn't owned RIGHT NOW.
|
|
48
|
+
constructor(host, opts = {}) {
|
|
49
|
+
this.host = host;
|
|
50
|
+
this.opts = opts;
|
|
51
|
+
}
|
|
52
|
+
/** `tab_list` rows owned by the account (per-tab `account` attribution). */
|
|
53
|
+
async ownedTabs(account) {
|
|
54
|
+
const list = await this.host.sendCommand({ method: 'tab_list', account });
|
|
55
|
+
const rows = Array.isArray(list.tabs)
|
|
56
|
+
? list.tabs
|
|
57
|
+
: [];
|
|
58
|
+
const owned = [];
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
if (row?.account !== account)
|
|
61
|
+
continue;
|
|
62
|
+
const targetId = typeof row.tabId === 'string' && row.tabId ? row.tabId : undefined;
|
|
63
|
+
if (!targetId)
|
|
64
|
+
continue;
|
|
65
|
+
owned.push({
|
|
66
|
+
targetId,
|
|
67
|
+
shortId: row.tab === undefined ? undefined : String(row.tab),
|
|
68
|
+
url: typeof row.url === 'string' ? row.url : undefined,
|
|
69
|
+
active: row.active === true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return owned;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the full CDP targetId a command must run against.
|
|
76
|
+
*
|
|
77
|
+
* With `ref` (caller-supplied `tabId`/`tab`): it must be one of the account's
|
|
78
|
+
* OWN tabs (full targetId or short id) — anything else throws
|
|
79
|
+
* BrowserTargetError. Numeric refs are rejected outright: bb-browser resolves
|
|
80
|
+
* bare numbers as indices into the GLOBAL page-target array, which can address
|
|
81
|
+
* another context's page.
|
|
82
|
+
*
|
|
83
|
+
* Without `ref`: the owned tab Chrome marks active, else the first owned
|
|
84
|
+
* tab, else a fresh `about:blank` created INSIDE the account's
|
|
85
|
+
* BrowserContext (never the default context). Stateless — derived from the
|
|
86
|
+
* live tab_list every time.
|
|
87
|
+
*/
|
|
88
|
+
async resolveCommandTarget(account, ref) {
|
|
89
|
+
if (ref !== undefined && ref !== null && ref !== '') {
|
|
90
|
+
return this.resolveExplicitRef(account, ref);
|
|
91
|
+
}
|
|
92
|
+
const owned = await this.ownedTabs(account);
|
|
93
|
+
const pick = owned.find((t) => t.active) ?? owned[0];
|
|
94
|
+
if (pick)
|
|
95
|
+
return pick.targetId;
|
|
96
|
+
return this.createOwnedTab(account);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate + canonicalize a caller-supplied tab ref against owned tabs only.
|
|
100
|
+
* The ref is matched as a string against owned full/short ids and NEVER
|
|
101
|
+
* forwarded raw — so bb-browser's numeric-index fallback (which indexes into
|
|
102
|
+
* the GLOBAL page array, any context) is unreachable.
|
|
103
|
+
*/
|
|
104
|
+
async resolveExplicitRef(account, ref) {
|
|
105
|
+
const wanted = String(ref);
|
|
106
|
+
const owned = await this.ownedTabs(account);
|
|
107
|
+
const match = owned.find((t) => t.targetId === wanted || t.shortId === wanted);
|
|
108
|
+
if (!match) {
|
|
109
|
+
const indexHint = /^\d+$/.test(wanted)
|
|
110
|
+
? ' (numeric tab indices are not supported — pass the tab id returned by tab_list/tab_new)'
|
|
111
|
+
: '';
|
|
112
|
+
throw new BrowserTargetError(`tab ${wanted} does not belong to browser profile ${account} — commands are bound to the profile's own pages${indexHint}`, 'BROWSER_TARGET_FOREIGN');
|
|
113
|
+
}
|
|
114
|
+
return match.targetId;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Ensure the account owns at least one page tab, returning its targetId. A
|
|
118
|
+
* fresh account (or one whose pages were all closed) gets an `about:blank`
|
|
119
|
+
* tab created in ITS BrowserContext via `tab_new {account}` — bb-browser's
|
|
120
|
+
* `createTabInContext` re-activates the context (cookies re-injected) when it
|
|
121
|
+
* was disposed, so recovery never lands in the default context.
|
|
122
|
+
*/
|
|
123
|
+
async ensureOwnedTab(account) {
|
|
124
|
+
const owned = await this.ownedTabs(account);
|
|
125
|
+
const existing = owned.find((t) => t.active) ?? owned[0];
|
|
126
|
+
if (existing)
|
|
127
|
+
return existing.targetId;
|
|
128
|
+
return this.createOwnedTab(account);
|
|
129
|
+
}
|
|
130
|
+
async createOwnedTab(account) {
|
|
131
|
+
const created = await this.host.sendCommand({
|
|
132
|
+
method: 'tab_new',
|
|
133
|
+
account,
|
|
134
|
+
url: 'about:blank',
|
|
135
|
+
});
|
|
136
|
+
const targetId = typeof created.tabId === 'string' && created.tabId ? created.tabId : undefined;
|
|
137
|
+
if (targetId)
|
|
138
|
+
return targetId;
|
|
139
|
+
// tab_new responded without a usable tabId — re-derive from ownership rather
|
|
140
|
+
// than guess (never fall back to an unowned page).
|
|
141
|
+
const owned = await this.ownedTabs(account);
|
|
142
|
+
const pick = owned.find((t) => t.active) ?? owned[0];
|
|
143
|
+
if (!pick) {
|
|
144
|
+
throw new BrowserTargetError(`could not create a page for browser profile ${account} in its own context`, 'BROWSER_TARGET_UNAVAILABLE');
|
|
145
|
+
}
|
|
146
|
+
return pick.targetId;
|
|
147
|
+
}
|
|
148
|
+
/** Owned tab currently on `host` (www-tolerant), optionally only a specific ref. */
|
|
149
|
+
async findOwnedTabOnHost(account, host, onlyTargetId) {
|
|
150
|
+
const owned = await this.ownedTabs(account);
|
|
151
|
+
for (const tab of owned) {
|
|
152
|
+
if (onlyTargetId !== undefined && tab.targetId !== onlyTargetId)
|
|
153
|
+
continue;
|
|
154
|
+
if (!tab.url)
|
|
155
|
+
continue;
|
|
156
|
+
try {
|
|
157
|
+
if (hostsMatch(new URL(tab.url).host, host))
|
|
158
|
+
return tab.targetId;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* non-URL tab (about:blank etc.) — skip */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/** Whether any owned tab is already on `url` (same resource, http→https tolerant). */
|
|
167
|
+
async findOwnedTabMatchingUrl(account, url) {
|
|
168
|
+
const expected = comparableUrl(url);
|
|
169
|
+
if (!expected)
|
|
170
|
+
return undefined;
|
|
171
|
+
const owned = await this.ownedTabs(account);
|
|
172
|
+
for (const tab of owned) {
|
|
173
|
+
if (!tab.url)
|
|
174
|
+
continue;
|
|
175
|
+
const actual = comparableUrl(tab.url);
|
|
176
|
+
if (!actual)
|
|
177
|
+
continue;
|
|
178
|
+
const sameResource = hostsMatch(actual.host, expected.host) && actual.target === expected.target;
|
|
179
|
+
const sameProtocol = actual.protocol === expected.protocol;
|
|
180
|
+
const redirectedUpgrade = expected.protocol === 'http:' && actual.protocol === 'https:';
|
|
181
|
+
if (sameResource && (sameProtocol || redirectedUpgrade))
|
|
182
|
+
return tab.targetId;
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Account-scoped tab-by-domain resolution for `eval {domain}` (bb-browser's
|
|
188
|
+
* own `resolveTabByDomain` is account-blind — the reason this exists). Two
|
|
189
|
+
* properties the bare `tab_new` + eval approach lacked, both caught on the
|
|
190
|
+
* staging closed-loop E2E (2026-06-04):
|
|
191
|
+
*
|
|
192
|
+
* 1. Reuse: an existing account tab already on the domain is reused instead
|
|
193
|
+
* of opening a new tab per eval (upstream reuses matching tabs too).
|
|
194
|
+
* 2. Load wait: after creating a tab, wait for the navigation to commit
|
|
195
|
+
* before eval — upstream waits (~10s poll + settle); without it the clip
|
|
196
|
+
* script races `about:blank` and relative fetches fail
|
|
197
|
+
* ("Failed to parse URL from /hot.json").
|
|
198
|
+
*/
|
|
199
|
+
async resolveDomainTab(account, domain) {
|
|
200
|
+
const url = normalizeDomainUrl(domain);
|
|
201
|
+
let host = '';
|
|
202
|
+
try {
|
|
203
|
+
host = new URL(url).host;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* fall through — tab_new with whatever bb-browser makes of it */
|
|
207
|
+
}
|
|
208
|
+
if (host) {
|
|
209
|
+
const existing = await this.findOwnedTabOnHost(account, host);
|
|
210
|
+
if (existing !== undefined)
|
|
211
|
+
return existing;
|
|
212
|
+
}
|
|
213
|
+
const tab = await this.host.sendCommand({ method: 'tab_new', url, account });
|
|
214
|
+
const targetId = typeof tab.tabId === 'string' && tab.tabId ? tab.tabId : undefined;
|
|
215
|
+
if (targetId === undefined || !host)
|
|
216
|
+
return targetId;
|
|
217
|
+
// Wait for the created tab to actually reach the domain (navigation
|
|
218
|
+
// committed). Mirrors upstream resolveTabByDomain's bounded wait + settle.
|
|
219
|
+
let committed = false;
|
|
220
|
+
const deadline = Date.now() + (this.opts.commitTimeoutMs ?? 10_000);
|
|
221
|
+
while (Date.now() < deadline) {
|
|
222
|
+
if ((await this.findOwnedTabOnHost(account, host, targetId)) !== undefined) {
|
|
223
|
+
committed = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
await sleep(this.opts.commitPollMs ?? 300);
|
|
227
|
+
}
|
|
228
|
+
if (!committed) {
|
|
229
|
+
// The tab never confirmed it reached the domain within the deadline — its
|
|
230
|
+
// document is still about:blank / a partial load. Returning it would let a
|
|
231
|
+
// subsequent eval run on the WRONG page. CLOSE it so it can't linger as an
|
|
232
|
+
// owned tab a later ref-less resolve would pick, then fail closed.
|
|
233
|
+
await this.closeTabBestEffort(account, targetId);
|
|
234
|
+
throw new BrowserTargetError(`domain ${domain} page did not commit navigation in browser profile ${account}`, 'BROWSER_TARGET_UNAVAILABLE');
|
|
235
|
+
}
|
|
236
|
+
await sleep(this.opts.commitSettleMs ?? 750); // post-commit settle (upstream uses a flat 2s)
|
|
237
|
+
return targetId;
|
|
238
|
+
}
|
|
239
|
+
/** Close a tab we created but could not commit, so it does not linger as a
|
|
240
|
+
* residual owned tab. Best-effort — a close failure (already gone / rejected)
|
|
241
|
+
* is swallowed. bb-browser's close command is `close` (→ Target.closeTarget). */
|
|
242
|
+
async closeTabBestEffort(account, targetId) {
|
|
243
|
+
try {
|
|
244
|
+
await this.host.sendCommand({ method: 'close', tabId: targetId, account });
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* the tab may already be gone / bb-browser rejected the close — ignore */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Commands that take no page target at all — everything else is tab-addressed
|
|
253
|
+
* and MUST carry an owned tabId. `tab_new`, `site_info`, and the `site_*` listing
|
|
254
|
+
* commands resolve/create their page inside the account's context upstream
|
|
255
|
+
* (createTabInContext / account-scoped adapter execution), so they pass `account`
|
|
256
|
+
* instead of a pinned tab. NOTE `tab_list` IS dispatched through bb-browser's
|
|
257
|
+
* `ensurePageTarget` (its global-current-tab side effect is harmless once every
|
|
258
|
+
* real command pins its own target). `site_run` is deliberately NOT tab-less:
|
|
259
|
+
* the manager resolves the adapter's domain via site_info and pins an owned tab
|
|
260
|
+
* (or fails closed for a domain-less adapter), closing the old global-current-tab
|
|
261
|
+
* fallthrough — see BrowserProfileManager.buildCommandRequest.
|
|
262
|
+
*/
|
|
263
|
+
export const TABLESS_COMMANDS = new Set([
|
|
264
|
+
'account_list',
|
|
265
|
+
'account_create',
|
|
266
|
+
'account_delete',
|
|
267
|
+
'account_info',
|
|
268
|
+
'tab_list',
|
|
269
|
+
'tab_new',
|
|
270
|
+
'site_list',
|
|
271
|
+
'site_search',
|
|
272
|
+
'site_info',
|
|
273
|
+
]);
|
|
274
|
+
/** Host comparison tolerant of a `www.` prefix on either side. */
|
|
275
|
+
export function hostsMatch(a, b) {
|
|
276
|
+
const norm = (h) => h.toLowerCase().replace(/^www\./, '');
|
|
277
|
+
return norm(a) === norm(b);
|
|
278
|
+
}
|
|
279
|
+
export function normalizeDomainUrl(domain) {
|
|
280
|
+
const trimmed = domain.trim();
|
|
281
|
+
if (!trimmed)
|
|
282
|
+
return 'about:blank';
|
|
283
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
284
|
+
}
|
|
285
|
+
export function comparableUrl(url) {
|
|
286
|
+
try {
|
|
287
|
+
const parsed = new URL(url);
|
|
288
|
+
return {
|
|
289
|
+
protocol: parsed.protocol,
|
|
290
|
+
host: parsed.host,
|
|
291
|
+
target: `${parsed.pathname}${parsed.search}${parsed.hash}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BrowserTargetRegistry } from './browser-target-registry.js';
|
|
1
2
|
/** Optional managed TURN relay config carried on a `stream.start` command. */
|
|
2
3
|
export interface ViewerTurnConfig {
|
|
3
4
|
url: string;
|
|
@@ -21,13 +22,19 @@ export interface ViewerStreamerHost {
|
|
|
21
22
|
method: string;
|
|
22
23
|
account?: string;
|
|
23
24
|
}): Promise<Record<string, unknown>>;
|
|
24
|
-
|
|
25
|
+
/** First-use preparation (desired-proxy reconcile + readiness), shared with the
|
|
26
|
+
* agent-invoke path — the viewer's first stream/nav on a hydrated account must
|
|
27
|
+
* reconcile the proxy + verify readiness too, not just confirm the account
|
|
28
|
+
* exists. Idempotent: the steady path after first use is cheap. */
|
|
29
|
+
prepareForUse(account: string): Promise<void>;
|
|
25
30
|
withDaemonRecovery<T>(operation: () => Promise<T>, label: string): Promise<T>;
|
|
26
31
|
/** cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
27
32
|
cdpEndpoint(): Promise<{
|
|
28
33
|
host: string;
|
|
29
34
|
port: number;
|
|
30
35
|
}>;
|
|
36
|
+
/** Shared owned-target resolution (same registry the agent-invoke path pins with). */
|
|
37
|
+
targets: BrowserTargetRegistry;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* bb-viewer's /command control plane is UNAUTHENTICATED, so on the shared agents
|
|
@@ -165,25 +172,17 @@ export declare class BrowserViewerStreamer {
|
|
|
165
172
|
private resolveAccountPageCdpUrl;
|
|
166
173
|
/**
|
|
167
174
|
* Full CDP targetId of the profile account's active page tab (the owned tab
|
|
168
|
-
* Chrome marks active, else the first owned tab
|
|
169
|
-
*
|
|
170
|
-
* path
|
|
175
|
+
* Chrome marks active, else the first owned tab; a tab-less account gets an
|
|
176
|
+
* `about:blank` created inside its own context first). Shared with the
|
|
177
|
+
* agent-invoke path via BrowserTargetRegistry — one ownership implementation.
|
|
171
178
|
*/
|
|
172
179
|
private accountActivePageTargetId;
|
|
173
|
-
/**
|
|
174
|
-
* Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
|
|
175
|
-
* Chrome marks `active` when it belongs to this account, else the first owned
|
|
176
|
-
* tab (the global active flag is account-blind — another profile's tab may
|
|
177
|
-
* hold it, which must not leak here).
|
|
178
|
-
*/
|
|
179
|
-
private firstAccountPageTargetId;
|
|
180
180
|
/**
|
|
181
181
|
* Full CDP targetId for a specific account tab, addressed by the bb-browser
|
|
182
|
-
* short tab id (`tab`) or full target id (`tabId`).
|
|
182
|
+
* short tab id (`tab`) or full target id (`tabId`). Throws on tabs the
|
|
183
|
+
* profile does not own (BrowserTargetError). Used by stream.switch.
|
|
183
184
|
*/
|
|
184
185
|
private accountTabTargetId;
|
|
185
|
-
/** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
|
|
186
|
-
private accountTabs;
|
|
187
186
|
/**
|
|
188
187
|
* Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
|
|
189
188
|
* is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-viewer-streamer.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-viewer-streamer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-viewer-streamer.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-viewer-streamer.ts"],"names":[],"mappings":"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"}
|
|
@@ -332,7 +332,7 @@ export class BrowserViewerStreamer {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
return this.host.withDaemonRecovery(async () => {
|
|
335
|
-
await this.host.
|
|
335
|
+
await this.host.prepareForUse(profileId);
|
|
336
336
|
const request = { ...(input ?? {}) };
|
|
337
337
|
if (command === 'open' || command === 'tab_new') {
|
|
338
338
|
assertViewerNavigableUrl(request.url);
|
|
@@ -375,7 +375,7 @@ export class BrowserViewerStreamer {
|
|
|
375
375
|
* is the per-tab attribution we filter on (reused by findAccountTabOnHost).
|
|
376
376
|
*/
|
|
377
377
|
async resolveAccountPageCdpUrl(profileId, tabRef) {
|
|
378
|
-
await this.host.
|
|
378
|
+
await this.host.prepareForUse(profileId);
|
|
379
379
|
const { host, port } = await this.host.cdpEndpoint();
|
|
380
380
|
const targetId = tabRef !== undefined
|
|
381
381
|
? await this.accountTabTargetId(profileId, tabRef)
|
|
@@ -384,72 +384,20 @@ export class BrowserViewerStreamer {
|
|
|
384
384
|
}
|
|
385
385
|
/**
|
|
386
386
|
* Full CDP targetId of the profile account's active page tab (the owned tab
|
|
387
|
-
* Chrome marks active, else the first owned tab
|
|
388
|
-
*
|
|
389
|
-
* path
|
|
387
|
+
* Chrome marks active, else the first owned tab; a tab-less account gets an
|
|
388
|
+
* `about:blank` created inside its own context first). Shared with the
|
|
389
|
+
* agent-invoke path via BrowserTargetRegistry — one ownership implementation.
|
|
390
390
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (targetId !== undefined)
|
|
394
|
-
return targetId;
|
|
395
|
-
await this.host.sendBrowserCommand({
|
|
396
|
-
method: 'tab_new',
|
|
397
|
-
account: profileId,
|
|
398
|
-
url: 'about:blank',
|
|
399
|
-
});
|
|
400
|
-
targetId = await this.firstAccountPageTargetId(profileId);
|
|
401
|
-
if (targetId === undefined) {
|
|
402
|
-
throw new Error(`no page tab found for profile account ${profileId}`);
|
|
403
|
-
}
|
|
404
|
-
return targetId;
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
|
|
408
|
-
* Chrome marks `active` when it belongs to this account, else the first owned
|
|
409
|
-
* tab (the global active flag is account-blind — another profile's tab may
|
|
410
|
-
* hold it, which must not leak here).
|
|
411
|
-
*/
|
|
412
|
-
async firstAccountPageTargetId(profileId) {
|
|
413
|
-
const tabs = await this.accountTabs(profileId);
|
|
414
|
-
let firstOwned;
|
|
415
|
-
for (const t of tabs) {
|
|
416
|
-
if (t.account !== profileId)
|
|
417
|
-
continue;
|
|
418
|
-
const tabId = t.tabId;
|
|
419
|
-
if (typeof tabId !== 'string' || !tabId)
|
|
420
|
-
continue;
|
|
421
|
-
if (t.active === true)
|
|
422
|
-
return tabId;
|
|
423
|
-
firstOwned ??= tabId;
|
|
424
|
-
}
|
|
425
|
-
return firstOwned;
|
|
391
|
+
accountActivePageTargetId(profileId) {
|
|
392
|
+
return this.host.targets.resolveCommandTarget(profileId);
|
|
426
393
|
}
|
|
427
394
|
/**
|
|
428
395
|
* Full CDP targetId for a specific account tab, addressed by the bb-browser
|
|
429
|
-
* short tab id (`tab`) or full target id (`tabId`).
|
|
396
|
+
* short tab id (`tab`) or full target id (`tabId`). Throws on tabs the
|
|
397
|
+
* profile does not own (BrowserTargetError). Used by stream.switch.
|
|
430
398
|
*/
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const tabs = await this.accountTabs(profileId);
|
|
434
|
-
for (const t of tabs) {
|
|
435
|
-
if (t.account !== profileId)
|
|
436
|
-
continue;
|
|
437
|
-
const tabId = typeof t.tabId === 'string' ? t.tabId : undefined;
|
|
438
|
-
const shortTab = t.tab === undefined ? undefined : String(t.tab);
|
|
439
|
-
if ((tabId && tabId === ref) || (shortTab && shortTab === ref)) {
|
|
440
|
-
if (!tabId)
|
|
441
|
-
break;
|
|
442
|
-
return tabId;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
throw new Error(`tab ${ref} not found for profile account ${profileId}`);
|
|
446
|
-
}
|
|
447
|
-
/** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
|
|
448
|
-
async accountTabs(profileId) {
|
|
449
|
-
const list = await this.host.sendBrowserCommand({ method: 'tab_list', account: profileId });
|
|
450
|
-
return Array.isArray(list.tabs)
|
|
451
|
-
? list.tabs
|
|
452
|
-
: [];
|
|
399
|
+
accountTabTargetId(profileId, tabRef) {
|
|
400
|
+
return this.host.targets.resolveCommandTarget(profileId, tabRef);
|
|
453
401
|
}
|
|
454
402
|
// ---- bb-viewer streamer lifecycle ----
|
|
455
403
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-main.d.ts","sourceRoot":"","sources":["../src/daemon-main.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"daemon-main.d.ts","sourceRoot":"","sources":["../src/daemon-main.ts"],"names":[],"mappings":";AA0BA,MAAM,MAAM,oBAAoB,GAAG;IACjC,sBAAsB,EAAE,OAAO,CAAC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AA8KF,wBAAsB,cAAc,CAClC,SAAS,EAAE,oBAAoB,EAC/B,IAAI,WAAwB,GAC3B,OAAO,CAAC,IAAI,CAAC,CASf"}
|
package/dist/daemon-main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createLogger, createOtelLogger, initAgentTelemetry, } from '@parall/agent-core';
|
|
2
|
+
import { configureHttpKeepAlive, createLogger, createOtelLogger, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
3
|
import { ParallClient } from '@parall/sdk';
|
|
4
4
|
import { runCLI } from './cli.js';
|
|
5
5
|
import { resolveBundleDir, resolveClaudeDaemonConfig, resolveWsUrl, } from './config.js';
|
|
@@ -75,6 +75,8 @@ async function runForever(config, client, log, signal, updater, gate) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
async function main(bootstrap) {
|
|
78
|
+
// Before any fetch: long-lived HTTP connections for control-plane calls.
|
|
79
|
+
configureHttpKeepAlive();
|
|
78
80
|
const config = resolveClaudeDaemonConfig(process.env);
|
|
79
81
|
const telemetry = await initAgentTelemetry('parall-daemon', 'daemon');
|
|
80
82
|
log = createOtelLogger('daemon', 'daemon');
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (same-machine) control plane for the BYOC daemon — the ONLY human
|
|
3
|
+
* control surface for local browser profiles per
|
|
4
|
+
* docs/engineering-design/daemon-control-authorization-design.md §7.1.
|
|
5
|
+
*
|
|
6
|
+
* Transport: a per-user Unix domain socket under the daemon config dir
|
|
7
|
+
* (`<daemonConfigDir>/run/control.sock`), directory mode 0700 + socket mode
|
|
8
|
+
* 0600, so only the same OS user (the person who installed the daemon) can
|
|
9
|
+
* connect. The Parall Desktop shell — Electron main, never the renderer — is
|
|
10
|
+
* the intended client. The trust boundary is deliberately the OS user account:
|
|
11
|
+
* anything running as that user already owns `config.json` (the mck_), so the
|
|
12
|
+
* socket adds no new exposure; the platform-identity check (requester ==
|
|
13
|
+
* machines.created_by) is enforced by the supervisor's handler on top.
|
|
14
|
+
*
|
|
15
|
+
* Protocol: newline-delimited JSON, one request per line, one response per
|
|
16
|
+
* line, over a short-lived connection:
|
|
17
|
+
* → {"id":"1","command":"profile.open","profile_id":"brp_x","requester_user_id":"usr_y"}
|
|
18
|
+
* ← {"id":"1","ok":true,"result":{...}}
|
|
19
|
+
* ← {"id":"1","ok":false,"error":{"code":"OWNER_MISMATCH","message":"..."}}
|
|
20
|
+
* Unknown fields are ignored (forward compat); unknown commands are rejected.
|
|
21
|
+
* Windows support (named pipe) is deliberately deferred — the Desktop daemon
|
|
22
|
+
* module is darwin-only today (design §7.1).
|
|
23
|
+
*/
|
|
24
|
+
export type LocalProfileAction = 'open' | 'stop' | 'reset';
|
|
25
|
+
/** Typed failure a handler may throw; anything else maps to INTERNAL. */
|
|
26
|
+
export declare class LocalControlError extends Error {
|
|
27
|
+
readonly code: string;
|
|
28
|
+
constructor(code: string, message: string);
|
|
29
|
+
}
|
|
30
|
+
export interface LocalControlHandlers {
|
|
31
|
+
/** Liveness + capability probe; the result rides back verbatim. */
|
|
32
|
+
ping(): Promise<Record<string, unknown>>;
|
|
33
|
+
/** Drive one local browser profile. Resolves when the action completed. */
|
|
34
|
+
profileControl(action: LocalProfileAction, profileId: string, requesterUserId: string): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Socket path SSOT — the Desktop shell derives the same path from the shared
|
|
37
|
+
* config dir (PRLL_DAEMON_CONFIG_DIR namespacing keeps staging/prod apart). */
|
|
38
|
+
export declare function localControlSocketPath(env?: NodeJS.ProcessEnv): string;
|
|
39
|
+
export interface LocalControlServerOptions {
|
|
40
|
+
socketPath: string;
|
|
41
|
+
handlers: LocalControlHandlers;
|
|
42
|
+
log: {
|
|
43
|
+
info(msg: string): void;
|
|
44
|
+
warn(msg: string): void;
|
|
45
|
+
error(msg: string): void;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare class LocalControlServer {
|
|
49
|
+
private readonly opts;
|
|
50
|
+
private server;
|
|
51
|
+
private readonly sockets;
|
|
52
|
+
constructor(opts: LocalControlServerOptions);
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
stop(): Promise<void>;
|
|
55
|
+
private handleConnection;
|
|
56
|
+
private handleLine;
|
|
57
|
+
private reply;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=local-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-control.d.ts","sourceRoot":"","sources":["../src/local-control.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,yEAAyE;AACzE,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACzC,2EAA2E;IAC3E,cAAc,CACZ,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAWD;gFACgF;AAChF,wBAAgB,sBAAsB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEnF;AA6BD,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACrF;AAED,qBAAa,kBAAkB;IAIjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAEpB,IAAI,EAAE,yBAAyB;IAEtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkEtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B,OAAO,CAAC,gBAAgB;YA0BV,UAAU;IAuDxB,OAAO,CAAC,KAAK;CAiBd"}
|