@parall/daemon 1.33.0 → 1.35.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 +9 -9
- package/bundle/parall-claude-agent.js +182 -94
- package/bundle/parall-codex-agent.js +177 -93
- package/bundle/parall-daemon.js +767 -83
- package/dist/clip-runtime/browser-profile-manager.d.ts +13 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +94 -69
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +188 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -0
- package/dist/clip-runtime/browser-viewer-streamer.js +595 -0
- package/dist/clip-runtime/subprocess.d.ts +11 -0
- package/dist/clip-runtime/subprocess.d.ts.map +1 -0
- package/dist/clip-runtime/subprocess.js +33 -0
- package/dist/supervisor.d.ts +10 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +48 -6
- package/package.json +6 -6
package/bundle/parall-daemon.js
CHANGED
|
@@ -27118,6 +27118,7 @@ var ENDPOINTS = {
|
|
|
27118
27118
|
// Org-scoped
|
|
27119
27119
|
ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
|
|
27120
27120
|
ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
|
|
27121
|
+
TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
|
|
27121
27122
|
ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
|
|
27122
27123
|
ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
|
|
27123
27124
|
ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
|
|
@@ -27162,6 +27163,7 @@ var ENDPOINTS = {
|
|
|
27162
27163
|
AGENT: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}`,
|
|
27163
27164
|
AGENT_API_KEYS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys`,
|
|
27164
27165
|
AGENT_API_KEY: (orgId, agentId, key) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/${key}`,
|
|
27166
|
+
AGENT_API_KEY_REGENERATE: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/regenerate`,
|
|
27165
27167
|
AGENT_AVATAR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/avatar`,
|
|
27166
27168
|
AGENT_ACTIVITY: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/activity`,
|
|
27167
27169
|
AGENT_MONITOR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/monitor`,
|
|
@@ -27201,6 +27203,7 @@ var ENDPOINTS = {
|
|
|
27201
27203
|
MACHINE_AGENT_WORKSPACE_SETUP: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/workspace/setup`,
|
|
27202
27204
|
MACHINE_LLM_SOURCE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/llm-source`,
|
|
27203
27205
|
MACHINE_PROVIDER_ENABLED: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/provider-enabled`,
|
|
27206
|
+
MACHINE_CAPABILITIES: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/capabilities`,
|
|
27204
27207
|
MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
|
|
27205
27208
|
MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
|
|
27206
27209
|
MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
|
|
@@ -27221,6 +27224,12 @@ var ENDPOINTS = {
|
|
|
27221
27224
|
MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
|
|
27222
27225
|
MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
|
|
27223
27226
|
MACHINES_ME_BROWSE_RESPONSE: (requestId) => `${API_BASE}/machines/me/browse-response/${requestId}`,
|
|
27227
|
+
// Hosted browser live-viewer control plane (api-server, NOT clip-service):
|
|
27228
|
+
// the web client drives WebRTC signaling + tab nav through VIEWER_COMMAND;
|
|
27229
|
+
// api-server brokers each command to the host daemon via the machine:{id}
|
|
27230
|
+
// request/reply bridge (mirrors filesystem browse), and the daemon replies on
|
|
27231
|
+
// VIEWER_RESPONSE. See docs/engineering-design/hosted-browser-provider-design.md.
|
|
27232
|
+
MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE: (requestId) => `${API_BASE}/machines/me/browser-profiles/viewer-response/${requestId}`,
|
|
27224
27233
|
// Tasks (org-scoped)
|
|
27225
27234
|
TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
|
|
27226
27235
|
TASK: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
|
|
@@ -27286,6 +27295,9 @@ var ENDPOINTS = {
|
|
|
27286
27295
|
// Wiki Path Scopes (AFCS ACL)
|
|
27287
27296
|
WIKI_PATH_SCOPES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes`,
|
|
27288
27297
|
WIKI_PATH_SCOPE: (orgId, wikiId, scopeId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes/${scopeId}`,
|
|
27298
|
+
// Wiki Path Restrictions (AFCS narrowing ACL — private subtrees)
|
|
27299
|
+
WIKI_RESTRICTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions`,
|
|
27300
|
+
WIKI_RESTRICTION: (orgId, wikiId, restrictionId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions/${restrictionId}`,
|
|
27289
27301
|
WIKI_ACCESS_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-status`,
|
|
27290
27302
|
WIKI_ACCESS_REQUESTS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-requests`,
|
|
27291
27303
|
// Wiki History (commits, file commits, blame)
|
|
@@ -27334,6 +27346,8 @@ var ENDPOINTS = {
|
|
|
27334
27346
|
PUSH_VAPID_KEY: `${API_BASE}/push/vapid-key`,
|
|
27335
27347
|
// Notification preferences
|
|
27336
27348
|
NOTIFICATION_PREFERENCES: `${API_BASE}/notification-preferences`,
|
|
27349
|
+
// Unified search (messages + tasks + wiki, org-scoped)
|
|
27350
|
+
SEARCH: (orgId) => `${API_BASE}/orgs/${orgId}/search`,
|
|
27337
27351
|
// Feature flags (org-scoped, server-evaluated)
|
|
27338
27352
|
FEATURE_FLAGS: (orgId) => `${API_BASE}/orgs/${orgId}/feature-flags`,
|
|
27339
27353
|
// Billing & Credits (org-scoped)
|
|
@@ -27349,6 +27363,7 @@ var ENDPOINTS = {
|
|
|
27349
27363
|
// Clips (org-scoped, served by clip-service)
|
|
27350
27364
|
CLIPS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips`,
|
|
27351
27365
|
CLIP: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}`,
|
|
27366
|
+
CLIPS_BULK_METADATA: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips/bulk-metadata`,
|
|
27352
27367
|
CLIP_AGENTS: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}/agents`,
|
|
27353
27368
|
AGENT_CLIPS: (orgId, agentId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips`,
|
|
27354
27369
|
AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
|
|
@@ -27361,6 +27376,9 @@ var ENDPOINTS = {
|
|
|
27361
27376
|
BROWSER_PROFILE_RESET: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/reset`,
|
|
27362
27377
|
BROWSER_PROFILE_CONSENTS: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents`,
|
|
27363
27378
|
BROWSER_PROFILE_CONSENT: (orgId, profileId, clipId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents/${clipId}`,
|
|
27379
|
+
// Live viewer command — on api-server (API_BASE), not clip-service: it rides
|
|
27380
|
+
// the machine control-plane request/reply bridge that lives in api-server.
|
|
27381
|
+
BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
|
|
27364
27382
|
// Clip registry (global, served by clip-service → Pinix Hub proxy)
|
|
27365
27383
|
CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
|
|
27366
27384
|
};
|
|
@@ -27437,6 +27455,7 @@ var WS_EVENTS = {
|
|
|
27437
27455
|
MACHINE_CONFIG_UPDATED: "machine.config.updated",
|
|
27438
27456
|
MACHINE_CLIP_SYNC: "machine.clip.sync",
|
|
27439
27457
|
MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
|
|
27458
|
+
MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
|
|
27440
27459
|
AGENT_NEW_SESSION: "agent.new_session",
|
|
27441
27460
|
CLIP_CREATED: "clip.created",
|
|
27442
27461
|
CLIP_REMOVED: "clip.removed",
|
|
@@ -27467,8 +27486,13 @@ var ParallClient = class _ParallClient {
|
|
|
27467
27486
|
/** Proactive refresh when token expires within this window (seconds). */
|
|
27468
27487
|
static REFRESH_THRESHOLD_S = 5 * 60;
|
|
27469
27488
|
static normalizeFetchError(err) {
|
|
27470
|
-
if (typeof DOMException !== "undefined" && err instanceof DOMException
|
|
27471
|
-
|
|
27489
|
+
if (typeof DOMException !== "undefined" && err instanceof DOMException) {
|
|
27490
|
+
if (err.name === "TimeoutError") {
|
|
27491
|
+
return new ApiError(0, "Request timed out", "REQUEST_TIMEOUT");
|
|
27492
|
+
}
|
|
27493
|
+
if (err.name === "AbortError") {
|
|
27494
|
+
return new ApiError(0, "Request aborted", "REQUEST_ABORTED");
|
|
27495
|
+
}
|
|
27472
27496
|
}
|
|
27473
27497
|
const apiError = new ApiError(0, "Network request failed", "NETWORK_ERROR");
|
|
27474
27498
|
if (err instanceof Error && err.message && err.message !== "Failed to fetch") {
|
|
@@ -27575,13 +27599,18 @@ var ParallClient = class _ParallClient {
|
|
|
27575
27599
|
url += `?${qs}`;
|
|
27576
27600
|
}
|
|
27577
27601
|
const headers = this.buildHeaders();
|
|
27602
|
+
const timeoutSignal = AbortSignal.timeout(opts?.timeoutMs ?? 15e3);
|
|
27603
|
+
const signal = opts?.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
|
|
27578
27604
|
let res;
|
|
27579
27605
|
try {
|
|
27580
27606
|
res = await fetch(url, {
|
|
27581
27607
|
method,
|
|
27582
27608
|
headers,
|
|
27583
27609
|
body: body ? JSON.stringify(body) : void 0,
|
|
27584
|
-
signal
|
|
27610
|
+
signal,
|
|
27611
|
+
// keepalive lets a request fired during page unload (e.g. the browser
|
|
27612
|
+
// viewer's stream.close on pagehide) outlive the document.
|
|
27613
|
+
keepalive: opts?.keepalive
|
|
27585
27614
|
});
|
|
27586
27615
|
} catch (err) {
|
|
27587
27616
|
throw _ParallClient.normalizeFetchError(err);
|
|
@@ -27752,6 +27781,10 @@ var ParallClient = class _ParallClient {
|
|
|
27752
27781
|
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
|
|
27753
27782
|
return res.data;
|
|
27754
27783
|
}
|
|
27784
|
+
async getTeams(orgId) {
|
|
27785
|
+
const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
|
|
27786
|
+
return res.data;
|
|
27787
|
+
}
|
|
27755
27788
|
async getOnlineMembers(orgId) {
|
|
27756
27789
|
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
|
|
27757
27790
|
return res.user_ids ?? [];
|
|
@@ -27990,6 +28023,10 @@ var ParallClient = class _ParallClient {
|
|
|
27990
28023
|
async createAgentApiKey(orgId, agentId) {
|
|
27991
28024
|
return this.request("POST", ENDPOINTS.AGENT_API_KEYS(orgId, agentId));
|
|
27992
28025
|
}
|
|
28026
|
+
/** Revokes all of the agent's active API keys and mints a replacement. */
|
|
28027
|
+
async regenerateAgentApiKey(orgId, agentId) {
|
|
28028
|
+
return this.request("POST", ENDPOINTS.AGENT_API_KEY_REGENERATE(orgId, agentId));
|
|
28029
|
+
}
|
|
27993
28030
|
async revokeAgentApiKey(orgId, agentId, key) {
|
|
27994
28031
|
return this.request("DELETE", ENDPOINTS.AGENT_API_KEY(orgId, agentId, key));
|
|
27995
28032
|
}
|
|
@@ -28161,6 +28198,17 @@ var ParallClient = class _ParallClient {
|
|
|
28161
28198
|
provider_enabled: providerEnabled
|
|
28162
28199
|
});
|
|
28163
28200
|
}
|
|
28201
|
+
/**
|
|
28202
|
+
* Replace the machine's capability set (admin). Primary use: healing a
|
|
28203
|
+
* machine that registered without `browser_provider` during the capability
|
|
28204
|
+
* migration's rolling-deploy window. Removing `agent_host` is rejected by the
|
|
28205
|
+
* server while agents are attached (409 AGENTS_STILL_ATTACHED).
|
|
28206
|
+
*/
|
|
28207
|
+
async patchMachineCapabilities(orgId, machineId, capabilities) {
|
|
28208
|
+
return this.request("PATCH", ENDPOINTS.MACHINE_CAPABILITIES(orgId, machineId), {
|
|
28209
|
+
capabilities
|
|
28210
|
+
});
|
|
28211
|
+
}
|
|
28164
28212
|
/** Get machine-level runtime auth state. */
|
|
28165
28213
|
async getMachineRuntimeAuth(orgId, machineId) {
|
|
28166
28214
|
return this.request("GET", ENDPOINTS.MACHINE_RUNTIME_AUTH(orgId, machineId));
|
|
@@ -28251,6 +28299,22 @@ var ParallClient = class _ParallClient {
|
|
|
28251
28299
|
async postBrowseResponse(requestId, response) {
|
|
28252
28300
|
return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSE_RESPONSE(requestId), response);
|
|
28253
28301
|
}
|
|
28302
|
+
/**
|
|
28303
|
+
* `POST /machines/me/browser-profiles/viewer-response/{requestId}` — daemon
|
|
28304
|
+
* reply to a `machine.browser_profile.viewer` control command. Wakes the
|
|
28305
|
+
* api-server request/reply bridge (mirrors {@link postBrowseResponse}).
|
|
28306
|
+
*/
|
|
28307
|
+
async postBrowserProfileViewerResponse(requestId, response) {
|
|
28308
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE(requestId), response);
|
|
28309
|
+
}
|
|
28310
|
+
/**
|
|
28311
|
+
* `POST /orgs/{orgId}/browser-profiles/{profileId}/viewer/command` — drive the
|
|
28312
|
+
* hosted browser live viewer (WebRTC signaling + tab nav). Authz: profile
|
|
28313
|
+
* owner or org admin. api-server brokers the command to the host daemon.
|
|
28314
|
+
*/
|
|
28315
|
+
async browserViewerCommand(orgId, profileId, req, opts) {
|
|
28316
|
+
return this.request("POST", ENDPOINTS.BROWSER_PROFILE_VIEWER_COMMAND(orgId, profileId), req, void 0, false, opts);
|
|
28317
|
+
}
|
|
28254
28318
|
async resizeMachine(orgId, machineId, spec) {
|
|
28255
28319
|
return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
|
|
28256
28320
|
}
|
|
@@ -28529,6 +28593,9 @@ var ParallClient = class _ParallClient {
|
|
|
28529
28593
|
async getWikiNodeSections(orgId, wikiId, params) {
|
|
28530
28594
|
return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
|
|
28531
28595
|
}
|
|
28596
|
+
async search(orgId, params, opts) {
|
|
28597
|
+
return this.request("GET", ENDPOINTS.SEARCH(orgId), void 0, params, false, opts);
|
|
28598
|
+
}
|
|
28532
28599
|
async searchWiki(orgId, wikiId, params) {
|
|
28533
28600
|
return this.request("GET", ENDPOINTS.WIKI_SEARCH(orgId, wikiId), void 0, params);
|
|
28534
28601
|
}
|
|
@@ -28624,6 +28691,17 @@ var ParallClient = class _ParallClient {
|
|
|
28624
28691
|
async deleteWikiPathScope(orgId, wikiId, scopeId) {
|
|
28625
28692
|
await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
|
|
28626
28693
|
}
|
|
28694
|
+
// ---- Wiki Path Restrictions (narrowing ACL — private subtrees) ----
|
|
28695
|
+
async getWikiRestrictions(orgId, wikiId) {
|
|
28696
|
+
const res = await this.request("GET", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId));
|
|
28697
|
+
return res.data;
|
|
28698
|
+
}
|
|
28699
|
+
async createWikiRestriction(orgId, wikiId, data) {
|
|
28700
|
+
return this.request("POST", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId), data);
|
|
28701
|
+
}
|
|
28702
|
+
async deleteWikiRestriction(orgId, wikiId, restrictionId) {
|
|
28703
|
+
await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
|
|
28704
|
+
}
|
|
28627
28705
|
async getWikiAccessStatus(orgId, wikiId, path15) {
|
|
28628
28706
|
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path15 ? { path: path15 } : void 0);
|
|
28629
28707
|
}
|
|
@@ -28786,6 +28864,10 @@ var ParallClient = class _ParallClient {
|
|
|
28786
28864
|
async updateClip(orgId, clipId, req) {
|
|
28787
28865
|
return this.request("PATCH", ENDPOINTS.CLIP(orgId, clipId), req);
|
|
28788
28866
|
}
|
|
28867
|
+
/** Atomically set display_name and/or description on multiple clip instances (application metadata). */
|
|
28868
|
+
async bulkUpdateClipMetadata(orgId, req) {
|
|
28869
|
+
return this.request("POST", ENDPOINTS.CLIPS_BULK_METADATA(orgId), req);
|
|
28870
|
+
}
|
|
28789
28871
|
async deleteClip(orgId, clipId) {
|
|
28790
28872
|
await this.request("DELETE", ENDPOINTS.CLIP(orgId, clipId));
|
|
28791
28873
|
}
|
|
@@ -29686,7 +29768,7 @@ async function runCLI(args) {
|
|
|
29686
29768
|
init_config();
|
|
29687
29769
|
|
|
29688
29770
|
// ts/daemon/dist/supervisor.js
|
|
29689
|
-
import { spawn as
|
|
29771
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
29690
29772
|
import * as fs12 from "node:fs";
|
|
29691
29773
|
import * as path13 from "node:path";
|
|
29692
29774
|
|
|
@@ -31989,13 +32071,561 @@ var ClipProvider = class _ClipProvider {
|
|
|
31989
32071
|
};
|
|
31990
32072
|
|
|
31991
32073
|
// ts/daemon/dist/clip-runtime/browser-profile-manager.js
|
|
31992
|
-
import { spawn as
|
|
32074
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
31993
32075
|
import { randomBytes } from "node:crypto";
|
|
31994
32076
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
31995
32077
|
import { createRequire } from "node:module";
|
|
31996
|
-
import * as net from "node:net";
|
|
31997
32078
|
import * as path9 from "node:path";
|
|
31998
32079
|
import { fileURLToPath } from "node:url";
|
|
32080
|
+
|
|
32081
|
+
// ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
|
|
32082
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
32083
|
+
|
|
32084
|
+
// ts/daemon/dist/clip-runtime/subprocess.js
|
|
32085
|
+
import * as net from "node:net";
|
|
32086
|
+
function formatErrorForLog(err) {
|
|
32087
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32088
|
+
return message.replace(/\s+/g, " ").slice(0, 300);
|
|
32089
|
+
}
|
|
32090
|
+
async function findFreePort() {
|
|
32091
|
+
return new Promise((resolve8, reject) => {
|
|
32092
|
+
const server = net.createServer();
|
|
32093
|
+
server.unref();
|
|
32094
|
+
server.on("error", reject);
|
|
32095
|
+
server.listen(0, "127.0.0.1", () => {
|
|
32096
|
+
const address = server.address();
|
|
32097
|
+
server.close(() => {
|
|
32098
|
+
if (address && typeof address === "object") {
|
|
32099
|
+
resolve8(address.port);
|
|
32100
|
+
} else {
|
|
32101
|
+
reject(new Error("failed to allocate free port"));
|
|
32102
|
+
}
|
|
32103
|
+
});
|
|
32104
|
+
});
|
|
32105
|
+
});
|
|
32106
|
+
}
|
|
32107
|
+
function sleep(ms) {
|
|
32108
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
32109
|
+
}
|
|
32110
|
+
|
|
32111
|
+
// ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
|
|
32112
|
+
var BB_VIEWER_HEALTH_TIMEOUT_MS = 1e4;
|
|
32113
|
+
var BB_VIEWER_COMMAND_TIMEOUT_MS = 15e3;
|
|
32114
|
+
var BB_VIEWER_UNANSWERED_REAP_MS = 9e4;
|
|
32115
|
+
var BB_VIEWER_TERM_GRACE_MS = 5e3;
|
|
32116
|
+
var BrowserViewerStreamer = class {
|
|
32117
|
+
host;
|
|
32118
|
+
// Live bb-viewer (WebRTC streamer) subprocess per profile. One per profile;
|
|
32119
|
+
// see StreamerState. Cleaned up on stream.close, stopProfile, resetProfile,
|
|
32120
|
+
// and stop().
|
|
32121
|
+
streamers = /* @__PURE__ */ new Map();
|
|
32122
|
+
// Set by shutdown(): an in-flight spawnStreamer has no map entry yet, so a
|
|
32123
|
+
// shutdown during its health poll would otherwise let the bb-viewer child
|
|
32124
|
+
// survive daemon stop (on BYOC nothing else reaps it).
|
|
32125
|
+
stopping = false;
|
|
32126
|
+
constructor(host) {
|
|
32127
|
+
this.host = host;
|
|
32128
|
+
}
|
|
32129
|
+
/**
|
|
32130
|
+
* Handle a viewer control command for a profile. Throws on any failure (the
|
|
32131
|
+
* supervisor maps a throw to a `{error:{message}}` reply); never returns a
|
|
32132
|
+
* partial result. `data.input` / `data.turn` / `data.session_id` come from
|
|
32133
|
+
* MachineBrowserProfileViewerData.
|
|
32134
|
+
*/
|
|
32135
|
+
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
32136
|
+
if (!profileId)
|
|
32137
|
+
throw new Error("browser profile id is required");
|
|
32138
|
+
switch (command) {
|
|
32139
|
+
case "stream.start":
|
|
32140
|
+
return this.viewerStreamStart(profileId, sessionId, turn);
|
|
32141
|
+
case "stream.answer":
|
|
32142
|
+
return this.viewerStreamAnswer(profileId, sessionId, input);
|
|
32143
|
+
case "stream.close":
|
|
32144
|
+
return this.viewerStreamClose(profileId, sessionId, input);
|
|
32145
|
+
case "stream.switch":
|
|
32146
|
+
return this.viewerStreamSwitch(profileId, sessionId, input);
|
|
32147
|
+
case "close":
|
|
32148
|
+
return this.viewerCloseTab(profileId, sessionId, input);
|
|
32149
|
+
case "tab_list":
|
|
32150
|
+
case "tab_new":
|
|
32151
|
+
case "open":
|
|
32152
|
+
case "reload":
|
|
32153
|
+
case "back":
|
|
32154
|
+
case "forward":
|
|
32155
|
+
return this.viewerForwardNav(profileId, sessionId, command, input);
|
|
32156
|
+
default:
|
|
32157
|
+
throw new Error(`unknown viewer command: ${command}`);
|
|
32158
|
+
}
|
|
32159
|
+
}
|
|
32160
|
+
/**
|
|
32161
|
+
* stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
|
|
32162
|
+
* one), resolve the profile's account-scoped page-target CDP ws URL, and run
|
|
32163
|
+
* the streamer's `connect` command. Records sessionId on the streamer entry.
|
|
32164
|
+
*/
|
|
32165
|
+
async viewerStreamStart(profileId, sessionId, turn) {
|
|
32166
|
+
this.killProfileStreamer(profileId);
|
|
32167
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer stream.start ${profileId}`);
|
|
32168
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32169
|
+
const streamer = await this.spawnStreamer(profileId, sessionId, turn);
|
|
32170
|
+
try {
|
|
32171
|
+
const result = await this.streamerCommand(streamer, "connect", { cdpUrl });
|
|
32172
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32173
|
+
const reapTimer = setTimeout(() => {
|
|
32174
|
+
const current = this.streamers.get(profileId);
|
|
32175
|
+
if (current === streamer && !current.answered) {
|
|
32176
|
+
this.host.log.warn(`[bb-viewer] no stream.answer for ${profileId} within ${BB_VIEWER_UNANSWERED_REAP_MS / 1e3}s; reaping abandoned streamer`);
|
|
32177
|
+
this.killProfileStreamer(profileId);
|
|
32178
|
+
}
|
|
32179
|
+
}, BB_VIEWER_UNANSWERED_REAP_MS);
|
|
32180
|
+
reapTimer.unref?.();
|
|
32181
|
+
return { ...result, streamed_tab_id: streamer.streamedTabId };
|
|
32182
|
+
} catch (err) {
|
|
32183
|
+
this.killProfileStreamer(profileId);
|
|
32184
|
+
throw err;
|
|
32185
|
+
}
|
|
32186
|
+
}
|
|
32187
|
+
/**
|
|
32188
|
+
* stream.answer — apply the client's WebRTC answer to the profile's streamer.
|
|
32189
|
+
* Rejects if the streamer's sessionId no longer matches (a stale viewer
|
|
32190
|
+
* session from a superseded stream.start).
|
|
32191
|
+
*/
|
|
32192
|
+
async viewerStreamAnswer(profileId, sessionId, input) {
|
|
32193
|
+
const streamer = this.streamers.get(profileId);
|
|
32194
|
+
if (!streamer)
|
|
32195
|
+
throw new Error("no active viewer streamer for profile");
|
|
32196
|
+
if (streamer.sessionId !== sessionId)
|
|
32197
|
+
throw new Error("stale viewer session");
|
|
32198
|
+
streamer.answered = true;
|
|
32199
|
+
return this.streamerCommand(streamer, "answer", input ?? {});
|
|
32200
|
+
}
|
|
32201
|
+
/**
|
|
32202
|
+
* stream.close — best-effort stop the streamer, then kill + remove it. A
|
|
32203
|
+
* stale session's close (independent HTTP legs can arrive out of order after
|
|
32204
|
+
* a retry) must NOT tear down a successor session's live streamer, so a
|
|
32205
|
+
* mismatched sessionId is ignored. An EMPTY sessionId gets the same stale
|
|
32206
|
+
* treatment — a client closed mid-handshake never received a session id, and
|
|
32207
|
+
* its unscoped close racing the next session's stream.start would kill that
|
|
32208
|
+
* session's live streamer (React StrictMode's dev double-mount exercises
|
|
32209
|
+
* exactly this ordering). The only unscoped close is an explicit
|
|
32210
|
+
* `{force:true}` input, reserved for a server-initiated admin disconnect.
|
|
32211
|
+
*/
|
|
32212
|
+
async viewerStreamClose(profileId, sessionId, input) {
|
|
32213
|
+
const streamer = this.streamers.get(profileId);
|
|
32214
|
+
if (streamer) {
|
|
32215
|
+
const force = input?.force === true;
|
|
32216
|
+
if (!force && streamer.sessionId !== sessionId) {
|
|
32217
|
+
return { ok: true, stale: true };
|
|
32218
|
+
}
|
|
32219
|
+
try {
|
|
32220
|
+
await this.streamerCommand(streamer, "stop", {});
|
|
32221
|
+
} catch (err) {
|
|
32222
|
+
this.host.log.warn(`[bb-viewer] stop command failed for ${profileId} (killing anyway): ${formatErrorForLog(err)}`);
|
|
32223
|
+
}
|
|
32224
|
+
this.killProfileStreamer(profileId);
|
|
32225
|
+
}
|
|
32226
|
+
return { ok: true };
|
|
32227
|
+
}
|
|
32228
|
+
/**
|
|
32229
|
+
* stream.switch — point the existing streamer at a different account-scoped
|
|
32230
|
+
* tab. input {tab} is the bb-browser short tab id / target id. Guarded by
|
|
32231
|
+
* sessionId like stream.answer: a superseded session must not steer the
|
|
32232
|
+
* current streamer, and an EMPTY sessionId is rejected the same way — every
|
|
32233
|
+
* real caller has its session id by the time it can switch (it arrives with
|
|
32234
|
+
* the stream.start response), so a session-less switch is by definition not
|
|
32235
|
+
* the current viewer.
|
|
32236
|
+
*/
|
|
32237
|
+
async viewerStreamSwitch(profileId, sessionId, input) {
|
|
32238
|
+
const streamer = this.streamers.get(profileId);
|
|
32239
|
+
if (!streamer)
|
|
32240
|
+
throw new Error("no active viewer streamer for profile");
|
|
32241
|
+
if (!sessionId || streamer.sessionId !== sessionId)
|
|
32242
|
+
throw new Error("stale viewer session");
|
|
32243
|
+
const tab = input?.tab;
|
|
32244
|
+
if (tab === void 0 || tab === null || tab === "") {
|
|
32245
|
+
throw new Error("stream.switch requires input.tab");
|
|
32246
|
+
}
|
|
32247
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId, tab), `viewer stream.switch ${profileId}`);
|
|
32248
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32249
|
+
const result = await this.streamerCommand(streamer, "switch", { cdpUrl });
|
|
32250
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32251
|
+
return { ...result, streamed_tab_id: streamer.streamedTabId };
|
|
32252
|
+
}
|
|
32253
|
+
/**
|
|
32254
|
+
* close — close an account tab via bb-browser, then, if the closed tab was
|
|
32255
|
+
* the one being streamed, re-point the streamer at the profile's next active
|
|
32256
|
+
* tab. Without the re-switch the streamer stays bound to a destroyed CDP
|
|
32257
|
+
* target and the viewer goes black. Returns `streamed_tab_id` when a
|
|
32258
|
+
* re-switch happened so the client can adopt the new active tab.
|
|
32259
|
+
*/
|
|
32260
|
+
async viewerCloseTab(profileId, sessionId, input) {
|
|
32261
|
+
const ref = viewerTabRef(input ?? {});
|
|
32262
|
+
const closedTargetId = ref !== void 0 ? await this.accountTabTargetId(profileId, ref) : await this.accountActivePageTargetId(profileId);
|
|
32263
|
+
const result = await this.viewerForwardNav(profileId, sessionId, "close", {
|
|
32264
|
+
...input ?? {},
|
|
32265
|
+
tabId: closedTargetId
|
|
32266
|
+
});
|
|
32267
|
+
const streamer = this.streamers.get(profileId);
|
|
32268
|
+
if (!streamer || !streamer.streamedTabId || streamer.streamedTabId !== closedTargetId) {
|
|
32269
|
+
return result;
|
|
32270
|
+
}
|
|
32271
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer close re-switch ${profileId}`);
|
|
32272
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32273
|
+
const sw = await this.streamerCommand(streamer, "switch", { cdpUrl });
|
|
32274
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32275
|
+
return { ...result, ...sw, streamed_tab_id: streamer.streamedTabId };
|
|
32276
|
+
}
|
|
32277
|
+
/**
|
|
32278
|
+
* Bring a CDP page target to the foreground via Chrome's DevTools HTTP
|
|
32279
|
+
* endpoint (`/json/activate/{targetId}`, same host:port as the CDP ws). Chrome
|
|
32280
|
+
* does not composite backgrounded tabs, so `Page.startScreencast` on a
|
|
32281
|
+
* background tab yields zero frames (black viewer). Best-effort: a failure
|
|
32282
|
+
* must not block the stream. host:port + targetId are parsed from the cdpUrl
|
|
32283
|
+
* (`ws://host:port/devtools/page/<targetId>`) to avoid an extra /status call.
|
|
32284
|
+
*/
|
|
32285
|
+
async bringTargetToFront(cdpUrl) {
|
|
32286
|
+
const m = /^ws:\/\/([^/]+)\/devtools\/page\/(.+)$/.exec(cdpUrl);
|
|
32287
|
+
if (!m)
|
|
32288
|
+
return;
|
|
32289
|
+
const [, hostPort, targetId] = m;
|
|
32290
|
+
try {
|
|
32291
|
+
const resp = await fetch(`http://${hostPort}/json/activate/${targetId}`, {
|
|
32292
|
+
method: "GET",
|
|
32293
|
+
signal: AbortSignal.timeout(5e3)
|
|
32294
|
+
});
|
|
32295
|
+
if (!resp.ok) {
|
|
32296
|
+
this.host.log.warn(`[bb-viewer] activate target ${targetId} returned ${resp.status} (continuing)`);
|
|
32297
|
+
}
|
|
32298
|
+
} catch (err) {
|
|
32299
|
+
this.host.log.warn(`[bb-viewer] activate target ${targetId} failed (continuing): ${formatErrorForLog(err)}`);
|
|
32300
|
+
}
|
|
32301
|
+
}
|
|
32302
|
+
/**
|
|
32303
|
+
* Forward a navigation command (tab_list/tab_new/open/reload/back/forward/
|
|
32304
|
+
* close) to bb-browser scoped to the profile's account. This lets the human
|
|
32305
|
+
* in the live viewer navigate and handle OAuth popups directly. Reuses the
|
|
32306
|
+
* same account-scoped sendCommand path the agent invokes use.
|
|
32307
|
+
*
|
|
32308
|
+
* SECURITY — the `account` field alone does NOT scope tab addressing:
|
|
32309
|
+
* bb-browser's ensurePageTarget resolves a client-supplied tab/tabId against
|
|
32310
|
+
* EVERY page target in the shared Chrome (all accounts), and tab_list
|
|
32311
|
+
* returns every tab with `account` as a mere annotation (verified in
|
|
32312
|
+
* @pinixai/bb-browser-pro@0.15.0 dist/daemon.js ensurePageTarget/tab_list).
|
|
32313
|
+
* On a multi-profile host that would let one profile's viewer list,
|
|
32314
|
+
* navigate, and close other profiles' logged-in tabs. So before forwarding:
|
|
32315
|
+
* (a) any tab ref must resolve through accountTabTargetId, which throws on
|
|
32316
|
+
* tabs the profile's account does not own; (b) ref-less tab-addressed
|
|
32317
|
+
* commands are pinned to the profile's own active tab instead of
|
|
32318
|
+
* bb-browser's account-blind global current tab; (c) tab_list output is
|
|
32319
|
+
* filtered to the profile's own rows; and (d) open/tab_new URLs must be
|
|
32320
|
+
* http(s)/about:blank — file:// or chrome:// would read the host
|
|
32321
|
+
* filesystem / browser internals straight into the video stream.
|
|
32322
|
+
*/
|
|
32323
|
+
async viewerForwardNav(profileId, sessionId, command, input) {
|
|
32324
|
+
if (command !== "tab_list") {
|
|
32325
|
+
const streamer = this.streamers.get(profileId);
|
|
32326
|
+
if (streamer && streamer.sessionId !== sessionId) {
|
|
32327
|
+
throw new Error("stale viewer session");
|
|
32328
|
+
}
|
|
32329
|
+
}
|
|
32330
|
+
return this.host.withDaemonRecovery(async () => {
|
|
32331
|
+
await this.host.ensureAccount(profileId);
|
|
32332
|
+
const request3 = { ...input ?? {} };
|
|
32333
|
+
if (command === "open" || command === "tab_new") {
|
|
32334
|
+
assertViewerNavigableUrl(request3.url);
|
|
32335
|
+
}
|
|
32336
|
+
const ref = viewerTabRef(request3);
|
|
32337
|
+
if (ref !== void 0) {
|
|
32338
|
+
request3.tabId = await this.accountTabTargetId(profileId, ref);
|
|
32339
|
+
delete request3.tab;
|
|
32340
|
+
} else if (REFLESS_PIN_NAV.has(command)) {
|
|
32341
|
+
request3.tabId = await this.accountActivePageTargetId(profileId);
|
|
32342
|
+
}
|
|
32343
|
+
const result = await this.host.sendBrowserCommand({
|
|
32344
|
+
...request3,
|
|
32345
|
+
method: command,
|
|
32346
|
+
account: profileId
|
|
32347
|
+
});
|
|
32348
|
+
return command === "tab_list" ? filterTabListToAccount(result, profileId) : result;
|
|
32349
|
+
}, `viewer ${command} ${profileId}`);
|
|
32350
|
+
}
|
|
32351
|
+
/**
|
|
32352
|
+
* Resolve `ws://<cdpHost>:<cdpPort>/devtools/page/<targetId>` for a PAGE
|
|
32353
|
+
* target owned by THIS profile's account — NOT bb-browser's global current
|
|
32354
|
+
* tab (which `getCurrentTabCdpUrl`/`getTabCdpUrl` in the bundled daemon use
|
|
32355
|
+
* via `cdp.currentTargetId`, an account-blind global; see daemon.js
|
|
32356
|
+
* line ~10389/10410). When `tabRef` is given (stream.switch) we resolve that
|
|
32357
|
+
* specific account tab; otherwise the profile's active page tab.
|
|
32358
|
+
*
|
|
32359
|
+
* Evidence from the installed @pinixai/bb-browser-pro@0.15.0 dist/daemon.js:
|
|
32360
|
+
* - GET /status (handleStatus, line ~1659-1668) returns `cdpHost` + `cdpPort`
|
|
32361
|
+
* — the CDP host/port the daemon's Chrome listens on.
|
|
32362
|
+
* - The CDP page ws URL format is `ws://<cdp.host>:<cdp.port>/devtools/page/<page.id>`
|
|
32363
|
+
* (getCurrentTabCdpUrl, line ~10392), where `page.id` is the full CDP
|
|
32364
|
+
* targetId (getTargets maps `t.targetId` → `.id`, line ~2461-2466).
|
|
32365
|
+
* - tab_list (command handler, line ~1041-1059) returns per-tab
|
|
32366
|
+
* `{ tabId: t.id, tab: <shortId>, account: <accountName>, url, ... }`, so
|
|
32367
|
+
* `tabId` is exactly the full targetId for the devtools URL and `account`
|
|
32368
|
+
* is the per-tab attribution we filter on (reused by findAccountTabOnHost).
|
|
32369
|
+
*/
|
|
32370
|
+
async resolveAccountPageCdpUrl(profileId, tabRef) {
|
|
32371
|
+
await this.host.ensureAccount(profileId);
|
|
32372
|
+
const { host, port } = await this.host.cdpEndpoint();
|
|
32373
|
+
const targetId = tabRef !== void 0 ? await this.accountTabTargetId(profileId, tabRef) : await this.accountActivePageTargetId(profileId);
|
|
32374
|
+
return `ws://${host}:${port}/devtools/page/${targetId}`;
|
|
32375
|
+
}
|
|
32376
|
+
/**
|
|
32377
|
+
* Full CDP targetId of the profile account's active page tab (the owned tab
|
|
32378
|
+
* Chrome marks active, else the first owned tab). If the account owns no
|
|
32379
|
+
* page tab yet, open `about:blank` for it first (mirrors the agent-invoke
|
|
32380
|
+
* path's "always give a fresh account an owned tab" rule).
|
|
32381
|
+
*/
|
|
32382
|
+
async accountActivePageTargetId(profileId) {
|
|
32383
|
+
let targetId = await this.firstAccountPageTargetId(profileId);
|
|
32384
|
+
if (targetId !== void 0)
|
|
32385
|
+
return targetId;
|
|
32386
|
+
await this.host.sendBrowserCommand({
|
|
32387
|
+
method: "tab_new",
|
|
32388
|
+
account: profileId,
|
|
32389
|
+
url: "about:blank"
|
|
32390
|
+
});
|
|
32391
|
+
targetId = await this.firstAccountPageTargetId(profileId);
|
|
32392
|
+
if (targetId === void 0) {
|
|
32393
|
+
throw new Error(`no page tab found for profile account ${profileId}`);
|
|
32394
|
+
}
|
|
32395
|
+
return targetId;
|
|
32396
|
+
}
|
|
32397
|
+
/**
|
|
32398
|
+
* Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
|
|
32399
|
+
* Chrome marks `active` when it belongs to this account, else the first owned
|
|
32400
|
+
* tab (the global active flag is account-blind — another profile's tab may
|
|
32401
|
+
* hold it, which must not leak here).
|
|
32402
|
+
*/
|
|
32403
|
+
async firstAccountPageTargetId(profileId) {
|
|
32404
|
+
const tabs = await this.accountTabs(profileId);
|
|
32405
|
+
let firstOwned;
|
|
32406
|
+
for (const t of tabs) {
|
|
32407
|
+
if (t.account !== profileId)
|
|
32408
|
+
continue;
|
|
32409
|
+
const tabId = t.tabId;
|
|
32410
|
+
if (typeof tabId !== "string" || !tabId)
|
|
32411
|
+
continue;
|
|
32412
|
+
if (t.active === true)
|
|
32413
|
+
return tabId;
|
|
32414
|
+
firstOwned ??= tabId;
|
|
32415
|
+
}
|
|
32416
|
+
return firstOwned;
|
|
32417
|
+
}
|
|
32418
|
+
/**
|
|
32419
|
+
* Full CDP targetId for a specific account tab, addressed by the bb-browser
|
|
32420
|
+
* short tab id (`tab`) or full target id (`tabId`). Used by stream.switch.
|
|
32421
|
+
*/
|
|
32422
|
+
async accountTabTargetId(profileId, tabRef) {
|
|
32423
|
+
const ref = String(tabRef);
|
|
32424
|
+
const tabs = await this.accountTabs(profileId);
|
|
32425
|
+
for (const t of tabs) {
|
|
32426
|
+
if (t.account !== profileId)
|
|
32427
|
+
continue;
|
|
32428
|
+
const tabId = typeof t.tabId === "string" ? t.tabId : void 0;
|
|
32429
|
+
const shortTab = t.tab === void 0 ? void 0 : String(t.tab);
|
|
32430
|
+
if (tabId && tabId === ref || shortTab && shortTab === ref) {
|
|
32431
|
+
if (!tabId)
|
|
32432
|
+
break;
|
|
32433
|
+
return tabId;
|
|
32434
|
+
}
|
|
32435
|
+
}
|
|
32436
|
+
throw new Error(`tab ${ref} not found for profile account ${profileId}`);
|
|
32437
|
+
}
|
|
32438
|
+
/** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
|
|
32439
|
+
async accountTabs(profileId) {
|
|
32440
|
+
const list = await this.host.sendBrowserCommand({ method: "tab_list", account: profileId });
|
|
32441
|
+
return Array.isArray(list.tabs) ? list.tabs : [];
|
|
32442
|
+
}
|
|
32443
|
+
// ---- bb-viewer streamer lifecycle ----
|
|
32444
|
+
/**
|
|
32445
|
+
* Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
|
|
32446
|
+
* is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
|
|
32447
|
+
* defaulting to `bb-viewer` on PATH — we NEVER download it. A free port is
|
|
32448
|
+
* allocated per streamer. Stores the entry in `this.streamers` once healthy.
|
|
32449
|
+
*/
|
|
32450
|
+
async spawnStreamer(profileId, sessionId, turn) {
|
|
32451
|
+
if (this.stopping)
|
|
32452
|
+
throw new Error("viewer streamer is shutting down");
|
|
32453
|
+
const bin = process.env.PRLL_BB_VIEWER_BIN ?? "bb-viewer";
|
|
32454
|
+
const port = await findFreePort();
|
|
32455
|
+
const args = ["--api-only", "--port", String(port)];
|
|
32456
|
+
if (turn?.url) {
|
|
32457
|
+
args.push("--turn-url", turn.url, "--turn-user", turn.username ?? "", "--turn-cred", turn.credential ?? "");
|
|
32458
|
+
}
|
|
32459
|
+
const streamerEnv = {};
|
|
32460
|
+
for (const key of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "DISPLAY"]) {
|
|
32461
|
+
const val = process.env[key];
|
|
32462
|
+
if (val !== void 0)
|
|
32463
|
+
streamerEnv[key] = val;
|
|
32464
|
+
}
|
|
32465
|
+
this.host.log.info(`[bb-viewer] spawning for profile ${profileId} on port ${port}`);
|
|
32466
|
+
const child = spawn3(bin, args, { stdio: ["ignore", "pipe", "pipe"], env: streamerEnv });
|
|
32467
|
+
let childError = null;
|
|
32468
|
+
child.once("error", (err) => {
|
|
32469
|
+
childError = err;
|
|
32470
|
+
});
|
|
32471
|
+
child.once("exit", () => {
|
|
32472
|
+
const current = this.streamers.get(profileId);
|
|
32473
|
+
if (current?.child === child) {
|
|
32474
|
+
this.streamers.delete(profileId);
|
|
32475
|
+
this.host.log.warn(`[bb-viewer] streamer for ${profileId} exited; cleared stale entry`);
|
|
32476
|
+
}
|
|
32477
|
+
});
|
|
32478
|
+
child.stdout?.on("data", (chunk) => {
|
|
32479
|
+
const text = chunk.toString("utf8").trim();
|
|
32480
|
+
if (text)
|
|
32481
|
+
this.host.log.info(`[bb-viewer] ${text}`);
|
|
32482
|
+
});
|
|
32483
|
+
child.stderr?.on("data", (chunk) => {
|
|
32484
|
+
const text = chunk.toString("utf8").trim();
|
|
32485
|
+
if (text)
|
|
32486
|
+
this.host.log.warn(`[bb-viewer] ${text}`);
|
|
32487
|
+
});
|
|
32488
|
+
const deadline = Date.now() + BB_VIEWER_HEALTH_TIMEOUT_MS;
|
|
32489
|
+
while (Date.now() < deadline) {
|
|
32490
|
+
if (this.stopping) {
|
|
32491
|
+
this.killChild(child, "streamer for stopping daemon");
|
|
32492
|
+
throw new Error("viewer streamer is shutting down");
|
|
32493
|
+
}
|
|
32494
|
+
if (childError) {
|
|
32495
|
+
throw childError;
|
|
32496
|
+
}
|
|
32497
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
32498
|
+
throw new Error("bb-viewer exited during startup");
|
|
32499
|
+
}
|
|
32500
|
+
try {
|
|
32501
|
+
const resp = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
32502
|
+
signal: AbortSignal.timeout(2e3)
|
|
32503
|
+
});
|
|
32504
|
+
if (resp.ok) {
|
|
32505
|
+
const streamer = { child, port, sessionId };
|
|
32506
|
+
this.streamers.set(profileId, streamer);
|
|
32507
|
+
this.host.log.info(`[bb-viewer] ready for profile ${profileId} on port ${port}`);
|
|
32508
|
+
return streamer;
|
|
32509
|
+
}
|
|
32510
|
+
} catch {
|
|
32511
|
+
}
|
|
32512
|
+
await sleep(200);
|
|
32513
|
+
}
|
|
32514
|
+
this.killChild(child, "unhealthy streamer");
|
|
32515
|
+
throw new Error("bb-viewer did not become healthy in time");
|
|
32516
|
+
}
|
|
32517
|
+
/**
|
|
32518
|
+
* SIGTERM with a SIGKILL escalation: a bb-viewer that hangs or ignores TERM
|
|
32519
|
+
* would otherwise outlive its teardown holding the port + CDP session (the
|
|
32520
|
+
* shell-level pattern-kill only exists on the hosted pod, not BYOC). The
|
|
32521
|
+
* escalation timer is unref'd so it never holds the daemon open; it is
|
|
32522
|
+
* cleared on exit. Caveat: a daemon process that exits immediately after
|
|
32523
|
+
* shutdown() abandons the timer — acceptable, the TERM was still sent.
|
|
32524
|
+
*/
|
|
32525
|
+
killChild(child, label) {
|
|
32526
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
32527
|
+
return;
|
|
32528
|
+
try {
|
|
32529
|
+
child.kill("SIGTERM");
|
|
32530
|
+
} catch {
|
|
32531
|
+
return;
|
|
32532
|
+
}
|
|
32533
|
+
const killTimer = setTimeout(() => {
|
|
32534
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
32535
|
+
this.host.log.warn(`[bb-viewer] ${label} ignored SIGTERM; escalating to SIGKILL`);
|
|
32536
|
+
try {
|
|
32537
|
+
child.kill("SIGKILL");
|
|
32538
|
+
} catch {
|
|
32539
|
+
}
|
|
32540
|
+
}
|
|
32541
|
+
}, BB_VIEWER_TERM_GRACE_MS);
|
|
32542
|
+
killTimer.unref?.();
|
|
32543
|
+
child.once("exit", () => clearTimeout(killTimer));
|
|
32544
|
+
}
|
|
32545
|
+
/**
|
|
32546
|
+
* POST a command to the profile's bb-viewer `/command` endpoint. Parses the
|
|
32547
|
+
* `{result}` / `{error:{message}}` envelope (bb-viewer api.go) and returns
|
|
32548
|
+
* the inner result, throwing the error message on failure.
|
|
32549
|
+
*/
|
|
32550
|
+
async streamerCommand(streamer, method, params) {
|
|
32551
|
+
const controller = new AbortController();
|
|
32552
|
+
const timer = setTimeout(() => controller.abort(), BB_VIEWER_COMMAND_TIMEOUT_MS);
|
|
32553
|
+
try {
|
|
32554
|
+
const resp = await fetch(`http://127.0.0.1:${streamer.port}/command`, {
|
|
32555
|
+
method: "POST",
|
|
32556
|
+
headers: { "Content-Type": "application/json" },
|
|
32557
|
+
body: JSON.stringify({ method, params }),
|
|
32558
|
+
signal: controller.signal
|
|
32559
|
+
});
|
|
32560
|
+
const text = await resp.text();
|
|
32561
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
32562
|
+
if (parsed.error) {
|
|
32563
|
+
throw new Error(parsed.error.message || `bb-viewer ${method} failed`);
|
|
32564
|
+
}
|
|
32565
|
+
if (!resp.ok) {
|
|
32566
|
+
throw new Error(`bb-viewer ${method} returned ${resp.status}: ${text}`);
|
|
32567
|
+
}
|
|
32568
|
+
return parsed.result ?? {};
|
|
32569
|
+
} finally {
|
|
32570
|
+
clearTimeout(timer);
|
|
32571
|
+
}
|
|
32572
|
+
}
|
|
32573
|
+
/** Kill + remove the profile's bb-viewer streamer, if any. Idempotent. */
|
|
32574
|
+
killProfileStreamer(profileId) {
|
|
32575
|
+
const streamer = this.streamers.get(profileId);
|
|
32576
|
+
if (!streamer)
|
|
32577
|
+
return;
|
|
32578
|
+
this.streamers.delete(profileId);
|
|
32579
|
+
this.killChild(streamer.child, `streamer for ${profileId}`);
|
|
32580
|
+
}
|
|
32581
|
+
/**
|
|
32582
|
+
* Tear down every profile streamer and refuse new spawns (daemon stop).
|
|
32583
|
+
* Idempotent.
|
|
32584
|
+
*/
|
|
32585
|
+
shutdown() {
|
|
32586
|
+
this.stopping = true;
|
|
32587
|
+
for (const profileId of [...this.streamers.keys()]) {
|
|
32588
|
+
this.killProfileStreamer(profileId);
|
|
32589
|
+
}
|
|
32590
|
+
}
|
|
32591
|
+
};
|
|
32592
|
+
function targetIdFromCdpUrl(cdpUrl) {
|
|
32593
|
+
return /\/devtools\/page\/(.+)$/.exec(cdpUrl)?.[1];
|
|
32594
|
+
}
|
|
32595
|
+
var REFLESS_PIN_NAV = /* @__PURE__ */ new Set(["reload", "back", "forward", "close"]);
|
|
32596
|
+
function viewerTabRef(request3) {
|
|
32597
|
+
for (const key of ["tabId", "tab"]) {
|
|
32598
|
+
const v = request3[key];
|
|
32599
|
+
if (typeof v === "string" && v !== "" || typeof v === "number")
|
|
32600
|
+
return String(v);
|
|
32601
|
+
}
|
|
32602
|
+
return void 0;
|
|
32603
|
+
}
|
|
32604
|
+
var VIEWER_NAVIGABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
32605
|
+
function assertViewerNavigableUrl(raw) {
|
|
32606
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
32607
|
+
return;
|
|
32608
|
+
if (typeof raw !== "string")
|
|
32609
|
+
throw new Error("url must be a string");
|
|
32610
|
+
if (raw === "about:blank")
|
|
32611
|
+
return;
|
|
32612
|
+
let parsed;
|
|
32613
|
+
try {
|
|
32614
|
+
parsed = new URL(raw);
|
|
32615
|
+
} catch {
|
|
32616
|
+
throw new Error(`invalid url: ${raw}`);
|
|
32617
|
+
}
|
|
32618
|
+
if (!VIEWER_NAVIGABLE_PROTOCOLS.has(parsed.protocol)) {
|
|
32619
|
+
throw new Error(`url scheme not allowed in the live viewer: ${parsed.protocol.replace(/:$/, "")}`);
|
|
32620
|
+
}
|
|
32621
|
+
}
|
|
32622
|
+
function filterTabListToAccount(result, account) {
|
|
32623
|
+
const tabs = Array.isArray(result.tabs) ? result.tabs : [];
|
|
32624
|
+
const owned = tabs.filter((t) => t?.account === account);
|
|
32625
|
+
return { ...result, tabs: owned, activeIndex: owned.findIndex((t) => t?.active === true) };
|
|
32626
|
+
}
|
|
32627
|
+
|
|
32628
|
+
// ts/daemon/dist/clip-runtime/browser-profile-manager.js
|
|
31999
32629
|
var BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15e3;
|
|
32000
32630
|
var BrowserCommandError = class extends Error {
|
|
32001
32631
|
method;
|
|
@@ -32015,9 +32645,20 @@ var BrowserProfileManager = class {
|
|
|
32015
32645
|
ensuredAccounts = /* @__PURE__ */ new Set();
|
|
32016
32646
|
ensuringAccounts = /* @__PURE__ */ new Map();
|
|
32017
32647
|
reportedStatuses = /* @__PURE__ */ new Map();
|
|
32648
|
+
// Live viewer orchestration (bb-viewer streamer subprocesses + viewer nav)
|
|
32649
|
+
// lives in BrowserViewerStreamer; the manager hands it the account-scoped
|
|
32650
|
+
// bb-browser command path via the host interface.
|
|
32651
|
+
viewer;
|
|
32018
32652
|
constructor(opts) {
|
|
32019
32653
|
this.opts = opts;
|
|
32020
32654
|
mkdirSync5(this.opts.homeDir, { recursive: true });
|
|
32655
|
+
this.viewer = new BrowserViewerStreamer({
|
|
32656
|
+
log: this.opts.log,
|
|
32657
|
+
sendBrowserCommand: (request3) => this.sendCommand(request3),
|
|
32658
|
+
ensureAccount: (account) => this.ensureAccount(account),
|
|
32659
|
+
withDaemonRecovery: (operation, label) => this.withDaemonRecovery(operation, label),
|
|
32660
|
+
cdpEndpoint: () => this.cdpEndpoint()
|
|
32661
|
+
});
|
|
32021
32662
|
}
|
|
32022
32663
|
async invoke({ profileId, account, command, input }) {
|
|
32023
32664
|
if (!profileId)
|
|
@@ -32068,30 +32709,13 @@ var BrowserProfileManager = class {
|
|
|
32068
32709
|
throw new Error("browser profile id is required");
|
|
32069
32710
|
this.reportedStatuses.delete(profileId);
|
|
32070
32711
|
const url = normalizeStartUrl(startUrl);
|
|
32071
|
-
let prepareResult = {
|
|
32072
|
-
accountCreated: false,
|
|
32073
|
-
accountExistedBeforePrepare: true
|
|
32074
|
-
};
|
|
32075
|
-
let firstAccountExistedBeforePrepare;
|
|
32076
|
-
let recoveredDuringPrepare = false;
|
|
32077
32712
|
try {
|
|
32078
|
-
|
|
32079
|
-
|
|
32080
|
-
|
|
32081
|
-
|
|
32082
|
-
});
|
|
32083
|
-
} catch (err) {
|
|
32084
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
32085
|
-
this.reportStatus(profileId, "error", message);
|
|
32086
|
-
throw err;
|
|
32087
|
-
}
|
|
32088
|
-
try {
|
|
32089
|
-
await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated, recoveredDuringPrepare, firstAccountExistedBeforePrepare ?? prepareResult.accountExistedBeforePrepare);
|
|
32713
|
+
await this.withDaemonRecovery(async () => {
|
|
32714
|
+
const prepareResult = await this.prepareOpenProfile(profileId, url);
|
|
32715
|
+
await this.openProfileTabOnce(profileId, url, prepareResult.accountCreated);
|
|
32716
|
+
}, `open profile ${profileId}`);
|
|
32090
32717
|
this.reportStatus(profileId, "running");
|
|
32091
32718
|
} catch (err) {
|
|
32092
|
-
if (isRecoverableBrowserDaemonError(err)) {
|
|
32093
|
-
await this.restartDaemon(`open profile ${profileId}`, err);
|
|
32094
|
-
}
|
|
32095
32719
|
const message = err instanceof Error ? err.message : String(err);
|
|
32096
32720
|
this.reportStatus(profileId, "error", message);
|
|
32097
32721
|
throw err;
|
|
@@ -32101,12 +32725,14 @@ var BrowserProfileManager = class {
|
|
|
32101
32725
|
if (!profileId)
|
|
32102
32726
|
throw new Error("browser profile id is required");
|
|
32103
32727
|
this.reportedStatuses.delete(profileId);
|
|
32728
|
+
this.viewer.killProfileStreamer(profileId);
|
|
32104
32729
|
this.reportStatus(profileId, "stopped");
|
|
32105
32730
|
}
|
|
32106
32731
|
async resetProfile(profileId) {
|
|
32107
32732
|
if (!profileId)
|
|
32108
32733
|
throw new Error("browser profile id is required");
|
|
32109
32734
|
this.reportedStatuses.delete(profileId);
|
|
32735
|
+
this.viewer.killProfileStreamer(profileId);
|
|
32110
32736
|
try {
|
|
32111
32737
|
await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
|
|
32112
32738
|
this.reportStatus(profileId, "stopped");
|
|
@@ -32117,6 +32743,7 @@ var BrowserProfileManager = class {
|
|
|
32117
32743
|
}
|
|
32118
32744
|
}
|
|
32119
32745
|
async stop() {
|
|
32746
|
+
this.viewer.shutdown();
|
|
32120
32747
|
const daemon = this.daemon;
|
|
32121
32748
|
this.daemon = null;
|
|
32122
32749
|
this.starting = null;
|
|
@@ -32133,23 +32760,35 @@ var BrowserProfileManager = class {
|
|
|
32133
32760
|
daemon.child.kill("SIGTERM");
|
|
32134
32761
|
}
|
|
32135
32762
|
}
|
|
32136
|
-
|
|
32137
|
-
|
|
32138
|
-
|
|
32763
|
+
/**
|
|
32764
|
+
* Viewer control command entrypoint — delegates to BrowserViewerStreamer
|
|
32765
|
+
* (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
|
|
32766
|
+
* subprocesses and viewer tab navigation.
|
|
32767
|
+
*/
|
|
32768
|
+
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
32769
|
+
return this.viewer.handleViewerCommand(profileId, sessionId, command, input, turn);
|
|
32770
|
+
}
|
|
32771
|
+
/** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
32772
|
+
async cdpEndpoint() {
|
|
32773
|
+
const daemon = await this.ensureDaemon();
|
|
32774
|
+
const status = await this.post("/status", daemon, void 0, 5e3);
|
|
32775
|
+
const host = status.cdpHost?.trim();
|
|
32776
|
+
const port = status.cdpPort;
|
|
32777
|
+
if (!host || typeof port !== "number" || !Number.isFinite(port)) {
|
|
32778
|
+
throw new Error("bb-browser /status did not report a CDP endpoint");
|
|
32779
|
+
}
|
|
32780
|
+
return { host, port };
|
|
32781
|
+
}
|
|
32782
|
+
async prepareOpenProfile(profileId, url) {
|
|
32139
32783
|
const accountCreated = await this.ensureAccount(profileId, url !== "about:blank" ? url : void 0);
|
|
32140
32784
|
await this.sendCommand({ method: "tab_list", account: profileId });
|
|
32141
|
-
return { accountCreated
|
|
32785
|
+
return { accountCreated };
|
|
32142
32786
|
}
|
|
32143
|
-
async openProfileTabOnce(profileId, url, accountCreated
|
|
32787
|
+
async openProfileTabOnce(profileId, url, accountCreated) {
|
|
32144
32788
|
if (accountCreated)
|
|
32145
32789
|
return;
|
|
32146
|
-
if (
|
|
32147
|
-
|
|
32148
|
-
return;
|
|
32149
|
-
const host = safeUrlHost(url);
|
|
32150
|
-
if (host && await this.findAccountTabOnHost(profileId, host) !== void 0)
|
|
32151
|
-
return;
|
|
32152
|
-
}
|
|
32790
|
+
if (await this.profileAlreadyHasOpenTab(profileId, url))
|
|
32791
|
+
return;
|
|
32153
32792
|
await this.sendCommand({ method: "tab_new", account: profileId, url });
|
|
32154
32793
|
}
|
|
32155
32794
|
async resetProfileOnce(profileId) {
|
|
@@ -32291,12 +32930,40 @@ var BrowserProfileManager = class {
|
|
|
32291
32930
|
}
|
|
32292
32931
|
}
|
|
32293
32932
|
async ensureAccountOwnedTab(account) {
|
|
32294
|
-
|
|
32295
|
-
const tabs = Array.isArray(list.tabs) ? list.tabs : [];
|
|
32296
|
-
if (tabs.some((tab) => tab?.account === account))
|
|
32933
|
+
if (await this.hasAccountOwnedTab(account))
|
|
32297
32934
|
return;
|
|
32298
32935
|
await this.sendCommand({ method: "tab_new", account, url: "about:blank" });
|
|
32299
32936
|
}
|
|
32937
|
+
async profileAlreadyHasOpenTab(profileId, url) {
|
|
32938
|
+
if (url === "about:blank")
|
|
32939
|
+
return this.hasAccountOwnedTab(profileId);
|
|
32940
|
+
return this.findAccountTabMatchingUrl(profileId, url);
|
|
32941
|
+
}
|
|
32942
|
+
async hasAccountOwnedTab(account) {
|
|
32943
|
+
const list = await this.sendCommand({ method: "tab_list", account });
|
|
32944
|
+
const tabs = Array.isArray(list.tabs) ? list.tabs : [];
|
|
32945
|
+
return tabs.some((tab) => tab?.account === account);
|
|
32946
|
+
}
|
|
32947
|
+
async findAccountTabMatchingUrl(account, url) {
|
|
32948
|
+
const expected = comparableUrl(url);
|
|
32949
|
+
if (!expected)
|
|
32950
|
+
return false;
|
|
32951
|
+
const list = await this.sendCommand({ method: "tab_list", account });
|
|
32952
|
+
const tabs = Array.isArray(list.tabs) ? list.tabs : [];
|
|
32953
|
+
for (const tab of tabs) {
|
|
32954
|
+
if (tab?.account !== account || typeof tab.url !== "string")
|
|
32955
|
+
continue;
|
|
32956
|
+
const actual = comparableUrl(tab.url);
|
|
32957
|
+
if (!actual)
|
|
32958
|
+
continue;
|
|
32959
|
+
const sameResource = hostsMatch(actual.host, expected.host) && actual.target === expected.target;
|
|
32960
|
+
const sameProtocol = actual.protocol === expected.protocol;
|
|
32961
|
+
const redirectedUpgrade = expected.protocol === "http:" && actual.protocol === "https:";
|
|
32962
|
+
if (sameResource && (sameProtocol || redirectedUpgrade))
|
|
32963
|
+
return true;
|
|
32964
|
+
}
|
|
32965
|
+
return false;
|
|
32966
|
+
}
|
|
32300
32967
|
reportStatus(profileId, status, errorMsg) {
|
|
32301
32968
|
const marker = `${status}\0${errorMsg ?? ""}`;
|
|
32302
32969
|
if (this.reportedStatuses.get(profileId) === marker)
|
|
@@ -32328,14 +32995,13 @@ var BrowserProfileManager = class {
|
|
|
32328
32995
|
this.starting = null;
|
|
32329
32996
|
}
|
|
32330
32997
|
}
|
|
32331
|
-
async withDaemonRecovery(operation, label
|
|
32998
|
+
async withDaemonRecovery(operation, label) {
|
|
32332
32999
|
try {
|
|
32333
33000
|
return await operation();
|
|
32334
33001
|
} catch (err) {
|
|
32335
33002
|
if (!isRecoverableBrowserDaemonError(err))
|
|
32336
33003
|
throw err;
|
|
32337
33004
|
await this.restartDaemon(label, err);
|
|
32338
|
-
onRecovered?.();
|
|
32339
33005
|
return operation();
|
|
32340
33006
|
}
|
|
32341
33007
|
}
|
|
@@ -32355,7 +33021,7 @@ var BrowserProfileManager = class {
|
|
|
32355
33021
|
this.starting = null;
|
|
32356
33022
|
this.ensuredAccounts.clear();
|
|
32357
33023
|
this.ensuringAccounts.clear();
|
|
32358
|
-
this.opts.log.warn(`[bb-browser] ${label} hit
|
|
33024
|
+
this.opts.log.warn(`[bb-browser] ${label} hit an unavailable Chrome/CDP page target; restarting bb-browser-daemon (${formatErrorForLog(err)})`);
|
|
32359
33025
|
if (!daemon)
|
|
32360
33026
|
return;
|
|
32361
33027
|
try {
|
|
@@ -32371,7 +33037,7 @@ var BrowserProfileManager = class {
|
|
|
32371
33037
|
const port = await findFreePort();
|
|
32372
33038
|
const token = randomToken();
|
|
32373
33039
|
const daemonPath = resolveBbBrowserDaemonPath();
|
|
32374
|
-
const child =
|
|
33040
|
+
const child = spawn4(process.execPath, [daemonPath, "--host", host, "--port", String(port), "--token", token], {
|
|
32375
33041
|
env: {
|
|
32376
33042
|
...process.env,
|
|
32377
33043
|
BB_BROWSER_HOME: this.opts.homeDir
|
|
@@ -32471,19 +33137,20 @@ function isBrowserAccountInfoUnauthenticatedError(err) {
|
|
|
32471
33137
|
function isRecoverableBrowserDaemonError(err) {
|
|
32472
33138
|
const message = err instanceof Error ? err.message : String(err);
|
|
32473
33139
|
const lower = message.toLowerCase();
|
|
32474
|
-
return lower.includes("chrome not connected") || lower.includes("cdp at 127.0.0.1") || lower.includes("bb-browser /command returned 503") && (lower.includes("chrome") || lower.includes("cdp"));
|
|
33140
|
+
return lower.includes("chrome not connected") || lower.includes("cdp at 127.0.0.1") || lower.includes("no page target found") || lower.includes("bb-browser /command returned 503") && (lower.includes("chrome") || lower.includes("cdp"));
|
|
32475
33141
|
}
|
|
32476
|
-
function
|
|
33142
|
+
function comparableUrl(url) {
|
|
32477
33143
|
try {
|
|
32478
|
-
|
|
33144
|
+
const parsed = new URL(url);
|
|
33145
|
+
return {
|
|
33146
|
+
protocol: parsed.protocol,
|
|
33147
|
+
host: parsed.host,
|
|
33148
|
+
target: `${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
33149
|
+
};
|
|
32479
33150
|
} catch {
|
|
32480
|
-
return
|
|
33151
|
+
return null;
|
|
32481
33152
|
}
|
|
32482
33153
|
}
|
|
32483
|
-
function formatErrorForLog(err) {
|
|
32484
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
32485
|
-
return message.replace(/\s+/g, " ").slice(0, 300);
|
|
32486
|
-
}
|
|
32487
33154
|
function resolveBbBrowserDaemonPath() {
|
|
32488
33155
|
const sibling = path9.join(path9.dirname(fileURLToPath(import.meta.url)), "bb-browser-daemon.js");
|
|
32489
33156
|
if (existsSync8(sibling))
|
|
@@ -32498,29 +33165,9 @@ function resolveBbBrowserDaemonPath() {
|
|
|
32498
33165
|
}
|
|
32499
33166
|
return full;
|
|
32500
33167
|
}
|
|
32501
|
-
async function findFreePort() {
|
|
32502
|
-
return new Promise((resolve8, reject) => {
|
|
32503
|
-
const server = net.createServer();
|
|
32504
|
-
server.unref();
|
|
32505
|
-
server.on("error", reject);
|
|
32506
|
-
server.listen(0, "127.0.0.1", () => {
|
|
32507
|
-
const address = server.address();
|
|
32508
|
-
server.close(() => {
|
|
32509
|
-
if (address && typeof address === "object") {
|
|
32510
|
-
resolve8(address.port);
|
|
32511
|
-
} else {
|
|
32512
|
-
reject(new Error("failed to allocate free port"));
|
|
32513
|
-
}
|
|
32514
|
-
});
|
|
32515
|
-
});
|
|
32516
|
-
});
|
|
32517
|
-
}
|
|
32518
33168
|
function randomToken() {
|
|
32519
33169
|
return randomBytes(16).toString("hex");
|
|
32520
33170
|
}
|
|
32521
|
-
function sleep(ms) {
|
|
32522
|
-
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
32523
|
-
}
|
|
32524
33171
|
|
|
32525
33172
|
// ts/daemon/dist/supervisor.js
|
|
32526
33173
|
init_config();
|
|
@@ -32920,7 +33567,7 @@ function shellQuote(value) {
|
|
|
32920
33567
|
}
|
|
32921
33568
|
|
|
32922
33569
|
// ts/daemon/dist/workspace.js
|
|
32923
|
-
import { spawn as
|
|
33570
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
32924
33571
|
import { createHash as createHash3 } from "node:crypto";
|
|
32925
33572
|
import * as fs11 from "node:fs";
|
|
32926
33573
|
import * as path12 from "node:path";
|
|
@@ -33224,7 +33871,7 @@ function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
|
|
|
33224
33871
|
clearTimeout(killTimer);
|
|
33225
33872
|
fn();
|
|
33226
33873
|
};
|
|
33227
|
-
const child =
|
|
33874
|
+
const child = spawn5(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
33228
33875
|
timeoutTimer = setTimeout(() => {
|
|
33229
33876
|
timedOut = true;
|
|
33230
33877
|
child.kill("SIGTERM");
|
|
@@ -33377,7 +34024,12 @@ var DaemonSupervisor = class {
|
|
|
33377
34024
|
children = /* @__PURE__ */ new Map();
|
|
33378
34025
|
spawningAgents = /* @__PURE__ */ new Set();
|
|
33379
34026
|
pendingWorkspaceSetup = /* @__PURE__ */ new Set();
|
|
33380
|
-
|
|
34027
|
+
// ONE queue per profile for BOTH lifecycle (open/stop/reset) and viewer
|
|
34028
|
+
// (stream.*/nav) commands. They mutate the same streamer + account state, so
|
|
34029
|
+
// two independent queues would let a stop/reset interleave with an in-flight
|
|
34030
|
+
// stream.start (the kill fires before the streamer registers and misses it,
|
|
34031
|
+
// leaving a live streamer on a profile the server just marked stopped).
|
|
34032
|
+
browserProfileOpQueues = /* @__PURE__ */ new Map();
|
|
33381
34033
|
workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
|
|
33382
34034
|
cancelledSpawns = /* @__PURE__ */ new Set();
|
|
33383
34035
|
ws = null;
|
|
@@ -33538,6 +34190,10 @@ var DaemonSupervisor = class {
|
|
|
33538
34190
|
this.log.info(`WS: browser profile lifecycle requested \u2014 profile=${data.profile_id} action=${data.action}`);
|
|
33539
34191
|
void this.enqueueBrowserProfileLifecycle(data);
|
|
33540
34192
|
});
|
|
34193
|
+
this.ws.on("machine.browser_profile.viewer", (data) => {
|
|
34194
|
+
this.log.info(`WS: browser profile viewer command \u2014 profile=${data.profile_id} command=${data.command}`);
|
|
34195
|
+
this.enqueueBrowserProfileViewer(data);
|
|
34196
|
+
});
|
|
33541
34197
|
this.ws.onStateChange((state) => {
|
|
33542
34198
|
if (state === "disconnected" || state === "reconnecting") {
|
|
33543
34199
|
this.log.warn(`machine WS state: ${state}`);
|
|
@@ -33849,17 +34505,45 @@ var DaemonSupervisor = class {
|
|
|
33849
34505
|
}
|
|
33850
34506
|
}
|
|
33851
34507
|
enqueueBrowserProfileLifecycle(data) {
|
|
33852
|
-
|
|
34508
|
+
return this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileLifecycle(data));
|
|
34509
|
+
}
|
|
34510
|
+
enqueueBrowserProfileOp(profileId, op) {
|
|
34511
|
+
const previous = this.browserProfileOpQueues.get(profileId) ?? Promise.resolve();
|
|
33853
34512
|
const next = previous.catch(() => {
|
|
33854
|
-
}).then(
|
|
33855
|
-
this.
|
|
34513
|
+
}).then(op);
|
|
34514
|
+
this.browserProfileOpQueues.set(profileId, next);
|
|
33856
34515
|
void next.finally(() => {
|
|
33857
|
-
if (this.
|
|
33858
|
-
this.
|
|
34516
|
+
if (this.browserProfileOpQueues.get(profileId) === next) {
|
|
34517
|
+
this.browserProfileOpQueues.delete(profileId);
|
|
33859
34518
|
}
|
|
33860
34519
|
});
|
|
33861
34520
|
return next;
|
|
33862
34521
|
}
|
|
34522
|
+
/**
|
|
34523
|
+
* Handle one viewer control command and reply via REST. Always answers the
|
|
34524
|
+
* request/reply bridge exactly once ({result} on success, {error:{message}}
|
|
34525
|
+
* on failure) and never throws into the WS event loop — mirrors
|
|
34526
|
+
* handleFilesystemBrowse. Serialized per profile by enqueueBrowserProfileViewer.
|
|
34527
|
+
*/
|
|
34528
|
+
async handleBrowserProfileViewer(data) {
|
|
34529
|
+
if (!this.browserProfileManager) {
|
|
34530
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, {
|
|
34531
|
+
error: { message: "Browser profile runtime is disabled on this daemon" }
|
|
34532
|
+
}).catch((err) => this.log.warn(`browser profile viewer negative ACK failed: ${String(err)}`));
|
|
34533
|
+
return;
|
|
34534
|
+
}
|
|
34535
|
+
try {
|
|
34536
|
+
const result = await this.browserProfileManager.handleViewerCommand(data.profile_id, data.session_id, data.command, data.input, data.turn);
|
|
34537
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, { result });
|
|
34538
|
+
} catch (err) {
|
|
34539
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34540
|
+
this.log.warn(`browser profile viewer command failed (profile=${data.profile_id}, command=${data.command}): ${message}`);
|
|
34541
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, { error: { message } }).catch((replyErr) => this.log.warn(`browser profile viewer error ACK failed: ${String(replyErr)}`));
|
|
34542
|
+
}
|
|
34543
|
+
}
|
|
34544
|
+
enqueueBrowserProfileViewer(data) {
|
|
34545
|
+
void this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileViewer(data));
|
|
34546
|
+
}
|
|
33863
34547
|
async handleClipSync(_data) {
|
|
33864
34548
|
await this.reconcileMachineClips();
|
|
33865
34549
|
}
|
|
@@ -34336,7 +35020,7 @@ var DaemonSupervisor = class {
|
|
|
34336
35020
|
const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
|
|
34337
35021
|
const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(" ")}` : adapter.bin;
|
|
34338
35022
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
|
|
34339
|
-
const child =
|
|
35023
|
+
const child = spawn6(adapter.bin, adapter.args, {
|
|
34340
35024
|
env,
|
|
34341
35025
|
stdio: ["ignore", "inherit", "inherit"],
|
|
34342
35026
|
detached: false
|