@parall/daemon 1.34.0 → 1.36.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 +13 -9
- package/bundle/parall-browser-pod.js +40665 -0
- package/bundle/parall-claude-agent.js +564 -91
- package/bundle/parall-codex-agent.js +559 -90
- package/bundle/parall-daemon.js +1000 -66
- package/dist/browser-pod.d.ts +294 -0
- package/dist/browser-pod.d.ts.map +1 -0
- package/dist/browser-pod.js +765 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +10 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +38 -26
- package/dist/clip-runtime/browser-state-store.d.ts +157 -0
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -0
- package/dist/clip-runtime/browser-state-store.js +370 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +222 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -0
- package/dist/clip-runtime/browser-viewer-streamer.js +691 -0
- package/dist/clip-runtime/clip-provider.d.ts +56 -0
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +141 -17
- 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 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +61 -19
- package/package.json +11 -8
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}`,
|
|
@@ -27247,6 +27256,17 @@ var ENDPOINTS = {
|
|
|
27247
27256
|
SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
|
|
27248
27257
|
SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
|
|
27249
27258
|
SCHEDULE_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/schedule_runs/${runId}`,
|
|
27259
|
+
// External triggers (org-scoped incoming integration primitive)
|
|
27260
|
+
EXTERNAL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/external-connections`,
|
|
27261
|
+
EXTERNAL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}`,
|
|
27262
|
+
EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/ingress-token/regenerate`,
|
|
27263
|
+
EXTERNAL_TRIGGER_SCHEMA: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/trigger-schema`,
|
|
27264
|
+
EXTERNAL_INGRESS_EVENTS: (orgId) => `${API_BASE}/orgs/${orgId}/external-ingress-events`,
|
|
27265
|
+
EXTERNAL_INGRESS_EVENT: (orgId, eventId) => `${API_BASE}/orgs/${orgId}/external-ingress-events/${eventId}`,
|
|
27266
|
+
EXTERNAL_TRIGGERS: (orgId) => `${API_BASE}/orgs/${orgId}/external-triggers`,
|
|
27267
|
+
EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
|
|
27268
|
+
EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
|
|
27269
|
+
EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
|
|
27250
27270
|
// Invitations (org-scoped, admin)
|
|
27251
27271
|
ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
|
|
27252
27272
|
ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
|
|
@@ -27286,6 +27306,9 @@ var ENDPOINTS = {
|
|
|
27286
27306
|
// Wiki Path Scopes (AFCS ACL)
|
|
27287
27307
|
WIKI_PATH_SCOPES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes`,
|
|
27288
27308
|
WIKI_PATH_SCOPE: (orgId, wikiId, scopeId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes/${scopeId}`,
|
|
27309
|
+
// Wiki Path Restrictions (AFCS narrowing ACL — private subtrees)
|
|
27310
|
+
WIKI_RESTRICTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions`,
|
|
27311
|
+
WIKI_RESTRICTION: (orgId, wikiId, restrictionId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions/${restrictionId}`,
|
|
27289
27312
|
WIKI_ACCESS_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-status`,
|
|
27290
27313
|
WIKI_ACCESS_REQUESTS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-requests`,
|
|
27291
27314
|
// Wiki History (commits, file commits, blame)
|
|
@@ -27351,6 +27374,7 @@ var ENDPOINTS = {
|
|
|
27351
27374
|
// Clips (org-scoped, served by clip-service)
|
|
27352
27375
|
CLIPS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips`,
|
|
27353
27376
|
CLIP: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}`,
|
|
27377
|
+
CLIPS_BULK_METADATA: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips/bulk-metadata`,
|
|
27354
27378
|
CLIP_AGENTS: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}/agents`,
|
|
27355
27379
|
AGENT_CLIPS: (orgId, agentId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips`,
|
|
27356
27380
|
AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
|
|
@@ -27363,6 +27387,9 @@ var ENDPOINTS = {
|
|
|
27363
27387
|
BROWSER_PROFILE_RESET: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/reset`,
|
|
27364
27388
|
BROWSER_PROFILE_CONSENTS: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents`,
|
|
27365
27389
|
BROWSER_PROFILE_CONSENT: (orgId, profileId, clipId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents/${clipId}`,
|
|
27390
|
+
// Live viewer command — on api-server (API_BASE), not clip-service: it rides
|
|
27391
|
+
// the machine control-plane request/reply bridge that lives in api-server.
|
|
27392
|
+
BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
|
|
27366
27393
|
// Clip registry (global, served by clip-service → Pinix Hub proxy)
|
|
27367
27394
|
CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
|
|
27368
27395
|
};
|
|
@@ -27439,6 +27466,7 @@ var WS_EVENTS = {
|
|
|
27439
27466
|
MACHINE_CONFIG_UPDATED: "machine.config.updated",
|
|
27440
27467
|
MACHINE_CLIP_SYNC: "machine.clip.sync",
|
|
27441
27468
|
MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
|
|
27469
|
+
MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
|
|
27442
27470
|
AGENT_NEW_SESSION: "agent.new_session",
|
|
27443
27471
|
CLIP_CREATED: "clip.created",
|
|
27444
27472
|
CLIP_REMOVED: "clip.removed",
|
|
@@ -27448,6 +27476,7 @@ var WS_EVENTS = {
|
|
|
27448
27476
|
// ts/sdk/dist/client.js
|
|
27449
27477
|
var ParallClient = class _ParallClient {
|
|
27450
27478
|
baseUrl;
|
|
27479
|
+
wikiBaseUrl;
|
|
27451
27480
|
token;
|
|
27452
27481
|
onTokenExpired;
|
|
27453
27482
|
getRefreshToken;
|
|
@@ -27499,12 +27528,22 @@ var ParallClient = class _ParallClient {
|
|
|
27499
27528
|
}
|
|
27500
27529
|
constructor(options = {}) {
|
|
27501
27530
|
this.baseUrl = options.baseUrl ?? "";
|
|
27531
|
+
this.wikiBaseUrl = options.wikiBaseUrl ?? this.baseUrl;
|
|
27502
27532
|
this.token = options.token ?? null;
|
|
27503
27533
|
this.onTokenExpired = options.onTokenExpired;
|
|
27504
27534
|
this.getRefreshToken = options.getRefreshToken;
|
|
27505
27535
|
this.setTokens = options.setTokens;
|
|
27506
27536
|
this.swimlaneName = options.swimlaneName;
|
|
27507
27537
|
}
|
|
27538
|
+
/**
|
|
27539
|
+
* Pick the origin for a request path: wiki-service base for `/wiki/v1`
|
|
27540
|
+
* endpoints, api base for everything else. The path itself (from ENDPOINTS)
|
|
27541
|
+
* is authoritative, so wiki vs api routing can't drift from how a caller
|
|
27542
|
+
* happens to invoke the client.
|
|
27543
|
+
*/
|
|
27544
|
+
baseUrlFor(path15) {
|
|
27545
|
+
return path15.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
|
|
27546
|
+
}
|
|
27508
27547
|
setToken(token) {
|
|
27509
27548
|
this.token = token;
|
|
27510
27549
|
}
|
|
@@ -27569,7 +27608,7 @@ var ParallClient = class _ParallClient {
|
|
|
27569
27608
|
if (!retried) {
|
|
27570
27609
|
await this.ensureFreshToken(path15);
|
|
27571
27610
|
}
|
|
27572
|
-
let url = `${this.
|
|
27611
|
+
let url = `${this.baseUrlFor(path15)}${path15}`;
|
|
27573
27612
|
if (query) {
|
|
27574
27613
|
const params = new URLSearchParams();
|
|
27575
27614
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -27590,7 +27629,10 @@ var ParallClient = class _ParallClient {
|
|
|
27590
27629
|
method,
|
|
27591
27630
|
headers,
|
|
27592
27631
|
body: body ? JSON.stringify(body) : void 0,
|
|
27593
|
-
signal
|
|
27632
|
+
signal,
|
|
27633
|
+
// keepalive lets a request fired during page unload (e.g. the browser
|
|
27634
|
+
// viewer's stream.close on pagehide) outlive the document.
|
|
27635
|
+
keepalive: opts?.keepalive
|
|
27594
27636
|
});
|
|
27595
27637
|
} catch (err) {
|
|
27596
27638
|
throw _ParallClient.normalizeFetchError(err);
|
|
@@ -27638,7 +27680,7 @@ var ParallClient = class _ParallClient {
|
|
|
27638
27680
|
void _drop;
|
|
27639
27681
|
let res;
|
|
27640
27682
|
try {
|
|
27641
|
-
res = await fetch(`${this.
|
|
27683
|
+
res = await fetch(`${this.baseUrlFor(path15)}${path15}`, {
|
|
27642
27684
|
method,
|
|
27643
27685
|
headers,
|
|
27644
27686
|
body,
|
|
@@ -27761,6 +27803,10 @@ var ParallClient = class _ParallClient {
|
|
|
27761
27803
|
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
|
|
27762
27804
|
return res.data;
|
|
27763
27805
|
}
|
|
27806
|
+
async getTeams(orgId) {
|
|
27807
|
+
const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
|
|
27808
|
+
return res.data;
|
|
27809
|
+
}
|
|
27764
27810
|
async getOnlineMembers(orgId) {
|
|
27765
27811
|
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
|
|
27766
27812
|
return res.user_ids ?? [];
|
|
@@ -27794,6 +27840,21 @@ var ParallClient = class _ParallClient {
|
|
|
27794
27840
|
q.limit = String(params.limit);
|
|
27795
27841
|
return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
|
|
27796
27842
|
}
|
|
27843
|
+
// Auto-paginated variant of getMemberTasks: fetches ALL pending tasks
|
|
27844
|
+
// (todo + in_progress) assigned to a member, including subtasks (the
|
|
27845
|
+
// endpoint does not filter parent_id). Powers the CLI `tasks assigned`
|
|
27846
|
+
// command so an agent answering "what's on X's plate" sees the full
|
|
27847
|
+
// backlog, not just the first page.
|
|
27848
|
+
async getMemberTasksAll(orgId, memberId) {
|
|
27849
|
+
const all = [];
|
|
27850
|
+
let cursor;
|
|
27851
|
+
do {
|
|
27852
|
+
const res = await this.getMemberTasks(orgId, memberId, { cursor, limit: 100 });
|
|
27853
|
+
all.push(...res.data);
|
|
27854
|
+
cursor = res.has_more ? res.next_cursor : void 0;
|
|
27855
|
+
} while (cursor);
|
|
27856
|
+
return all;
|
|
27857
|
+
}
|
|
27797
27858
|
// ---- Invitations ----
|
|
27798
27859
|
async createInvitation(orgId, email, role) {
|
|
27799
27860
|
return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
|
|
@@ -27999,6 +28060,10 @@ var ParallClient = class _ParallClient {
|
|
|
27999
28060
|
async createAgentApiKey(orgId, agentId) {
|
|
28000
28061
|
return this.request("POST", ENDPOINTS.AGENT_API_KEYS(orgId, agentId));
|
|
28001
28062
|
}
|
|
28063
|
+
/** Revokes all of the agent's active API keys and mints a replacement. */
|
|
28064
|
+
async regenerateAgentApiKey(orgId, agentId) {
|
|
28065
|
+
return this.request("POST", ENDPOINTS.AGENT_API_KEY_REGENERATE(orgId, agentId));
|
|
28066
|
+
}
|
|
28002
28067
|
async revokeAgentApiKey(orgId, agentId, key) {
|
|
28003
28068
|
return this.request("DELETE", ENDPOINTS.AGENT_API_KEY(orgId, agentId, key));
|
|
28004
28069
|
}
|
|
@@ -28170,6 +28235,17 @@ var ParallClient = class _ParallClient {
|
|
|
28170
28235
|
provider_enabled: providerEnabled
|
|
28171
28236
|
});
|
|
28172
28237
|
}
|
|
28238
|
+
/**
|
|
28239
|
+
* Replace the machine's capability set (admin). Primary use: healing a
|
|
28240
|
+
* machine that registered without `browser_provider` during the capability
|
|
28241
|
+
* migration's rolling-deploy window. Removing `agent_host` is rejected by the
|
|
28242
|
+
* server while agents are attached (409 AGENTS_STILL_ATTACHED).
|
|
28243
|
+
*/
|
|
28244
|
+
async patchMachineCapabilities(orgId, machineId, capabilities) {
|
|
28245
|
+
return this.request("PATCH", ENDPOINTS.MACHINE_CAPABILITIES(orgId, machineId), {
|
|
28246
|
+
capabilities
|
|
28247
|
+
});
|
|
28248
|
+
}
|
|
28173
28249
|
/** Get machine-level runtime auth state. */
|
|
28174
28250
|
async getMachineRuntimeAuth(orgId, machineId) {
|
|
28175
28251
|
return this.request("GET", ENDPOINTS.MACHINE_RUNTIME_AUTH(orgId, machineId));
|
|
@@ -28260,6 +28336,22 @@ var ParallClient = class _ParallClient {
|
|
|
28260
28336
|
async postBrowseResponse(requestId, response) {
|
|
28261
28337
|
return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSE_RESPONSE(requestId), response);
|
|
28262
28338
|
}
|
|
28339
|
+
/**
|
|
28340
|
+
* `POST /machines/me/browser-profiles/viewer-response/{requestId}` — daemon
|
|
28341
|
+
* reply to a `machine.browser_profile.viewer` control command. Wakes the
|
|
28342
|
+
* api-server request/reply bridge (mirrors {@link postBrowseResponse}).
|
|
28343
|
+
*/
|
|
28344
|
+
async postBrowserProfileViewerResponse(requestId, response) {
|
|
28345
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE(requestId), response);
|
|
28346
|
+
}
|
|
28347
|
+
/**
|
|
28348
|
+
* `POST /orgs/{orgId}/browser-profiles/{profileId}/viewer/command` — drive the
|
|
28349
|
+
* hosted browser live viewer (WebRTC signaling + tab nav). Authz: profile
|
|
28350
|
+
* owner or org admin. api-server brokers the command to the host daemon.
|
|
28351
|
+
*/
|
|
28352
|
+
async browserViewerCommand(orgId, profileId, req, opts) {
|
|
28353
|
+
return this.request("POST", ENDPOINTS.BROWSER_PROFILE_VIEWER_COMMAND(orgId, profileId), req, void 0, false, opts);
|
|
28354
|
+
}
|
|
28263
28355
|
async resizeMachine(orgId, machineId, spec) {
|
|
28264
28356
|
return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
|
|
28265
28357
|
}
|
|
@@ -28352,7 +28444,7 @@ var ParallClient = class _ParallClient {
|
|
|
28352
28444
|
* Returns null when the server responds with 304 (config unchanged).
|
|
28353
28445
|
*/
|
|
28354
28446
|
async getPlatformConfig(currentVersion) {
|
|
28355
|
-
const url = `${this.
|
|
28447
|
+
const url = `${this.baseUrlFor(ENDPOINTS.PLATFORM_CONFIG)}${ENDPOINTS.PLATFORM_CONFIG}`;
|
|
28356
28448
|
const extra = {};
|
|
28357
28449
|
if (currentVersion !== void 0) {
|
|
28358
28450
|
extra["If-None-Match"] = currentVersion;
|
|
@@ -28518,6 +28610,55 @@ var ParallClient = class _ParallClient {
|
|
|
28518
28610
|
async getScheduleRun(orgId, runId) {
|
|
28519
28611
|
return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
|
|
28520
28612
|
}
|
|
28613
|
+
// ---- External triggers (org-scoped) ----
|
|
28614
|
+
async createExternalConnection(orgId, input) {
|
|
28615
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), input);
|
|
28616
|
+
}
|
|
28617
|
+
async listExternalConnections(orgId, filters) {
|
|
28618
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), void 0, filters);
|
|
28619
|
+
}
|
|
28620
|
+
async getExternalConnection(orgId, connectionId) {
|
|
28621
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
|
|
28622
|
+
}
|
|
28623
|
+
async updateExternalConnection(orgId, connectionId, patch) {
|
|
28624
|
+
return this.request("PATCH", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId), patch);
|
|
28625
|
+
}
|
|
28626
|
+
async regenerateExternalConnectionIngressToken(orgId, connectionId) {
|
|
28627
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
|
|
28628
|
+
}
|
|
28629
|
+
async deleteExternalConnection(orgId, connectionId) {
|
|
28630
|
+
return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
|
|
28631
|
+
}
|
|
28632
|
+
async getExternalTriggerSchema(orgId, connectionId) {
|
|
28633
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
|
|
28634
|
+
}
|
|
28635
|
+
async listExternalIngressEvents(orgId, filters) {
|
|
28636
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENTS(orgId), void 0, filters);
|
|
28637
|
+
}
|
|
28638
|
+
async getExternalIngressEvent(orgId, eventId) {
|
|
28639
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENT(orgId, eventId));
|
|
28640
|
+
}
|
|
28641
|
+
async createExternalTrigger(orgId, input) {
|
|
28642
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), input);
|
|
28643
|
+
}
|
|
28644
|
+
async listExternalTriggers(orgId, filters) {
|
|
28645
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), void 0, filters);
|
|
28646
|
+
}
|
|
28647
|
+
async getExternalTrigger(orgId, triggerId) {
|
|
28648
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
|
|
28649
|
+
}
|
|
28650
|
+
async updateExternalTrigger(orgId, triggerId, patch) {
|
|
28651
|
+
return this.request("PATCH", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId), patch);
|
|
28652
|
+
}
|
|
28653
|
+
async deleteExternalTrigger(orgId, triggerId) {
|
|
28654
|
+
return this.request("DELETE", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
|
|
28655
|
+
}
|
|
28656
|
+
async listExternalTriggerRuns(orgId, filters) {
|
|
28657
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUNS(orgId), void 0, filters);
|
|
28658
|
+
}
|
|
28659
|
+
async getExternalTriggerRun(orgId, runId) {
|
|
28660
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUN(orgId, runId));
|
|
28661
|
+
}
|
|
28521
28662
|
// ---- Wikis (org-scoped) ----
|
|
28522
28663
|
async createWiki(orgId, data) {
|
|
28523
28664
|
return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
|
|
@@ -28532,8 +28673,35 @@ var ParallClient = class _ParallClient {
|
|
|
28532
28673
|
async getWikiTree(orgId, wikiId, params) {
|
|
28533
28674
|
return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
|
|
28534
28675
|
}
|
|
28676
|
+
/**
|
|
28677
|
+
* Resolve a server-returned, host-relative media URL (a wiki `signed_url`
|
|
28678
|
+
* like `/wiki/v1/signed/files?token=...`) against this client's base origin,
|
|
28679
|
+
* so it can be dropped straight into a browser `<img>`/`<video>`/`<iframe>`
|
|
28680
|
+
* `src`.
|
|
28681
|
+
*
|
|
28682
|
+
* wiki-service returns these relative on purpose — it doesn't know its own
|
|
28683
|
+
* public origin. A relative `src` resolves against the *page* origin, which
|
|
28684
|
+
* only works when the page and wiki-service share an origin (local dev:
|
|
28685
|
+
* same-origin + Next.js `/wiki/*` proxy). In deployed envs the app
|
|
28686
|
+
* (app.parall.com) and wiki-service (api.parall.com) are different origins,
|
|
28687
|
+
* so `app.parall.com/wiki/v1/signed/files` hits the SPA's own `/wiki/[...]`
|
|
28688
|
+
* catch-all route — an `<iframe>` then recursively renders the whole app
|
|
28689
|
+
* instead of the file. Prefixing with the wiki base (the exact origin every
|
|
28690
|
+
* wiki API request already uses — `baseUrlFor` resolves `/wiki/v1` paths to
|
|
28691
|
+
* `wikiBaseUrl`) makes the URL absolute against the origin that actually
|
|
28692
|
+
* serves the bytes. An empty base (local dev, same-origin proxy) leaves it
|
|
28693
|
+
* relative, preserving the proxy path.
|
|
28694
|
+
*/
|
|
28695
|
+
absoluteMediaUrl(url) {
|
|
28696
|
+
if (/^https?:\/\//i.test(url))
|
|
28697
|
+
return url;
|
|
28698
|
+
return `${this.baseUrlFor(url)}${url}`;
|
|
28699
|
+
}
|
|
28535
28700
|
async getWikiBlob(orgId, wikiId, params) {
|
|
28536
|
-
|
|
28701
|
+
const blob = await this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
|
|
28702
|
+
if (blob.signed_url)
|
|
28703
|
+
blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
|
|
28704
|
+
return blob;
|
|
28537
28705
|
}
|
|
28538
28706
|
async getWikiNodeSections(orgId, wikiId, params) {
|
|
28539
28707
|
return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
|
|
@@ -28623,7 +28791,10 @@ var ParallClient = class _ParallClient {
|
|
|
28623
28791
|
* token — don't leak it.
|
|
28624
28792
|
*/
|
|
28625
28793
|
async getWikiFilePreviewUrl(orgId, wikiId, params) {
|
|
28626
|
-
|
|
28794
|
+
const res = await this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
|
|
28795
|
+
if (res.url)
|
|
28796
|
+
res.url = this.absoluteMediaUrl(res.url);
|
|
28797
|
+
return res;
|
|
28627
28798
|
}
|
|
28628
28799
|
// ---- Wiki Path Scopes (AFCS ACL) ----
|
|
28629
28800
|
async getWikiPathScopes(orgId, wikiId) {
|
|
@@ -28636,6 +28807,17 @@ var ParallClient = class _ParallClient {
|
|
|
28636
28807
|
async deleteWikiPathScope(orgId, wikiId, scopeId) {
|
|
28637
28808
|
await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
|
|
28638
28809
|
}
|
|
28810
|
+
// ---- Wiki Path Restrictions (narrowing ACL — private subtrees) ----
|
|
28811
|
+
async getWikiRestrictions(orgId, wikiId) {
|
|
28812
|
+
const res = await this.request("GET", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId));
|
|
28813
|
+
return res.data;
|
|
28814
|
+
}
|
|
28815
|
+
async createWikiRestriction(orgId, wikiId, data) {
|
|
28816
|
+
return this.request("POST", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId), data);
|
|
28817
|
+
}
|
|
28818
|
+
async deleteWikiRestriction(orgId, wikiId, restrictionId) {
|
|
28819
|
+
await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
|
|
28820
|
+
}
|
|
28639
28821
|
async getWikiAccessStatus(orgId, wikiId, path15) {
|
|
28640
28822
|
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path15 ? { path: path15 } : void 0);
|
|
28641
28823
|
}
|
|
@@ -28798,6 +28980,10 @@ var ParallClient = class _ParallClient {
|
|
|
28798
28980
|
async updateClip(orgId, clipId, req) {
|
|
28799
28981
|
return this.request("PATCH", ENDPOINTS.CLIP(orgId, clipId), req);
|
|
28800
28982
|
}
|
|
28983
|
+
/** Atomically set display_name and/or description on multiple clip instances (application metadata). */
|
|
28984
|
+
async bulkUpdateClipMetadata(orgId, req) {
|
|
28985
|
+
return this.request("POST", ENDPOINTS.CLIPS_BULK_METADATA(orgId), req);
|
|
28986
|
+
}
|
|
28801
28987
|
async deleteClip(orgId, clipId) {
|
|
28802
28988
|
await this.request("DELETE", ENDPOINTS.CLIP(orgId, clipId));
|
|
28803
28989
|
}
|
|
@@ -28824,6 +29010,9 @@ var ParallClient = class _ParallClient {
|
|
|
28824
29010
|
const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
|
|
28825
29011
|
return resp.data;
|
|
28826
29012
|
}
|
|
29013
|
+
/** Org-wide browser-profile discovery list. Returns the sanitized
|
|
29014
|
+
* {@link BrowserProfileListItem} shape (not the full domain model), each row
|
|
29015
|
+
* carrying a per-viewer `can_open` control hint. */
|
|
28827
29016
|
async listBrowserProfiles(orgId) {
|
|
28828
29017
|
const resp = await this.request("GET", ENDPOINTS.BROWSER_PROFILES(orgId));
|
|
28829
29018
|
return resp.data;
|
|
@@ -29698,7 +29887,7 @@ async function runCLI(args) {
|
|
|
29698
29887
|
init_config();
|
|
29699
29888
|
|
|
29700
29889
|
// ts/daemon/dist/supervisor.js
|
|
29701
|
-
import { spawn as
|
|
29890
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
29702
29891
|
import * as fs12 from "node:fs";
|
|
29703
29892
|
import * as path13 from "node:path";
|
|
29704
29893
|
|
|
@@ -31622,6 +31811,10 @@ function encodeEnvelope2(msg) {
|
|
|
31622
31811
|
header.writeUInt32BE(json.length, 1);
|
|
31623
31812
|
return Buffer.concat([header, json]);
|
|
31624
31813
|
}
|
|
31814
|
+
function registrationKey(clips) {
|
|
31815
|
+
const sorted = [...clips].sort((a, b) => (a.alias ?? "").localeCompare(b.alias ?? ""));
|
|
31816
|
+
return JSON.stringify(sorted);
|
|
31817
|
+
}
|
|
31625
31818
|
var EnvelopeDecoder2 = class _EnvelopeDecoder {
|
|
31626
31819
|
buf = Buffer.alloc(0);
|
|
31627
31820
|
push(chunk) {
|
|
@@ -31657,7 +31850,15 @@ var ClipProvider = class _ClipProvider {
|
|
|
31657
31850
|
heartbeatTimer = null;
|
|
31658
31851
|
statusUnsubscribe = null;
|
|
31659
31852
|
manifestUnsubscribe = null;
|
|
31853
|
+
// Set when the clip manifest/set changed and the hub's copy is stale. Drained
|
|
31854
|
+
// by flushReregister(), which re-registers IN-STREAM (no reconnect).
|
|
31660
31855
|
needsReregister = false;
|
|
31856
|
+
// Canonical key of the clip-registration set last sent to the hub on the
|
|
31857
|
+
// CURRENT stream. flushReregister diff-gates against it so a cold-start that
|
|
31858
|
+
// re-derives an identical set (the common case — a clip's command surface is
|
|
31859
|
+
// stable across spawns) does not emit a redundant re-register. Reset per
|
|
31860
|
+
// stream; the first-frame register in openStream re-seeds it.
|
|
31861
|
+
lastRegisteredKey = null;
|
|
31661
31862
|
// Monotonic stream id, bumped by openStream(). Handlers from a stream that a
|
|
31662
31863
|
// newer openStream() has superseded bail in handleDisconnect(gen), so their
|
|
31663
31864
|
// late close/end/error can't reconnect against the healthy replacement.
|
|
@@ -31698,6 +31899,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31698
31899
|
if (this.stopped)
|
|
31699
31900
|
return;
|
|
31700
31901
|
const gen = ++this.streamGeneration;
|
|
31902
|
+
this.lastRegisteredKey = null;
|
|
31701
31903
|
try {
|
|
31702
31904
|
this.closeStream();
|
|
31703
31905
|
this.closeSession();
|
|
@@ -31796,21 +31998,57 @@ var ClipProvider = class _ClipProvider {
|
|
|
31796
31998
|
startHeartbeat() {
|
|
31797
31999
|
this.clearHeartbeatTimer();
|
|
31798
32000
|
this.heartbeatTimer = setInterval(() => {
|
|
31799
|
-
|
|
31800
|
-
this.needsReregister = false;
|
|
31801
|
-
this.opts.log.info("[clip-provider] manifest updated \u2014 reconnecting to re-register clips");
|
|
31802
|
-
this.connected = false;
|
|
31803
|
-
this.clearHeartbeatTimer();
|
|
31804
|
-
this.clearReconnectTimer();
|
|
31805
|
-
void this.openStream();
|
|
31806
|
-
return;
|
|
31807
|
-
}
|
|
32001
|
+
this.flushReregister();
|
|
31808
32002
|
this.sendProviderMessage({ ping: { sentAtUnixMs: Date.now() } }).catch((err) => {
|
|
31809
32003
|
this.opts.log.warn(`[clip-provider] heartbeat send failed: ${String(err)}`);
|
|
31810
32004
|
});
|
|
31811
32005
|
}, _ClipProvider.HEARTBEAT_INTERVAL_MS);
|
|
31812
32006
|
this.heartbeatTimer.unref?.();
|
|
31813
32007
|
}
|
|
32008
|
+
/**
|
|
32009
|
+
* Re-register the current clip set on the OPEN stream — the daemon's response
|
|
32010
|
+
* to a manifest/clip-set change. The hub overwrites its stored set from this
|
|
32011
|
+
* full register (clip-provider-sharing-design §11), so a clip install / wake /
|
|
32012
|
+
* sleep no longer requires tearing down and reopening the ProviderStream,
|
|
32013
|
+
* which used to drop any invoke in flight on that stream (in-flight requests
|
|
32014
|
+
* are keyed by request_id on the hub and survive a clip-set swap).
|
|
32015
|
+
*
|
|
32016
|
+
* No-op when the stream is not currently writable; needsReregister stays set
|
|
32017
|
+
* so the next heartbeat tick (or the post-connect flush) retries.
|
|
32018
|
+
*
|
|
32019
|
+
* Against an older hub that still rejects an in-stream duplicate register, the
|
|
32020
|
+
* write reaches a stream the hub then resets; the resulting stream error runs
|
|
32021
|
+
* the normal reconnect path, which re-registers as the first frame — i.e. it
|
|
32022
|
+
* degrades to the previous reconnect-to-re-register behavior, so no capability
|
|
32023
|
+
* negotiation is needed (deploy the hub first).
|
|
32024
|
+
*/
|
|
32025
|
+
flushReregister() {
|
|
32026
|
+
if (!this.needsReregister)
|
|
32027
|
+
return;
|
|
32028
|
+
if (!this.connected || !this.stream || this.stream.closed || this.stream.destroyed)
|
|
32029
|
+
return;
|
|
32030
|
+
const clips = this.buildClipRegistrations();
|
|
32031
|
+
if (registrationKey(clips) === this.lastRegisteredKey) {
|
|
32032
|
+
this.needsReregister = false;
|
|
32033
|
+
return;
|
|
32034
|
+
}
|
|
32035
|
+
this.needsReregister = false;
|
|
32036
|
+
this.opts.log.info("[clip-provider] re-registering clips in-stream");
|
|
32037
|
+
this.sendRegister(clips).catch((err) => {
|
|
32038
|
+
this.opts.log.warn(`[clip-provider] in-stream re-register failed: ${String(err)}`);
|
|
32039
|
+
this.needsReregister = true;
|
|
32040
|
+
});
|
|
32041
|
+
}
|
|
32042
|
+
/**
|
|
32043
|
+
* Re-register the current clip set on the open stream — public entry for the
|
|
32044
|
+
* supervisor when machine clip assignments change. Same in-stream path as a
|
|
32045
|
+
* manifest update (no reconnect, in-flight invokes survive). Safe when
|
|
32046
|
+
* disconnected: the flag is flushed on the next heartbeat / post-connect.
|
|
32047
|
+
*/
|
|
32048
|
+
reregister() {
|
|
32049
|
+
this.needsReregister = true;
|
|
32050
|
+
this.flushReregister();
|
|
32051
|
+
}
|
|
31814
32052
|
closeStream() {
|
|
31815
32053
|
if (this.stream) {
|
|
31816
32054
|
try {
|
|
@@ -31845,7 +32083,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31845
32083
|
});
|
|
31846
32084
|
});
|
|
31847
32085
|
this.manifestUnsubscribe = mgr.addManifestListener(() => {
|
|
31848
|
-
this.
|
|
32086
|
+
this.reregister();
|
|
31849
32087
|
});
|
|
31850
32088
|
}
|
|
31851
32089
|
unsubscribeStatusChanges() {
|
|
@@ -31862,6 +32100,8 @@ var ClipProvider = class _ClipProvider {
|
|
|
31862
32100
|
this.handleRegistered(msg.registerResponse);
|
|
31863
32101
|
} else if (msg.invokeCommand) {
|
|
31864
32102
|
void this.handleInvokeCommand(msg.invokeCommand);
|
|
32103
|
+
} else if (msg.viewerCommand) {
|
|
32104
|
+
void this.handleViewerCommand(msg.viewerCommand);
|
|
31865
32105
|
} else if (msg.pong !== void 0) {
|
|
31866
32106
|
}
|
|
31867
32107
|
}
|
|
@@ -31871,6 +32111,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31871
32111
|
this.connected = true;
|
|
31872
32112
|
this.reconnectAttempt = 0;
|
|
31873
32113
|
this.startHeartbeat();
|
|
32114
|
+
this.flushReregister();
|
|
31874
32115
|
} else {
|
|
31875
32116
|
this.opts.log.error(`[clip-provider] registration rejected: ${reg.message}`);
|
|
31876
32117
|
this.stopped = true;
|
|
@@ -31924,17 +32165,64 @@ var ClipProvider = class _ClipProvider {
|
|
|
31924
32165
|
});
|
|
31925
32166
|
}
|
|
31926
32167
|
}
|
|
32168
|
+
/**
|
|
32169
|
+
* Handle a hosted-browser live-viewer command relayed by clip-service over the
|
|
32170
|
+
* stream and reply on the same stream (design §3.4). Always answers the
|
|
32171
|
+
* request/reply bridge exactly once — `{result}` on success, `{error:{message}}`
|
|
32172
|
+
* on failure — and never throws into the message loop, mirroring the BYOC
|
|
32173
|
+
* daemon's handleBrowserProfileViewer (which replies over HTTP instead).
|
|
32174
|
+
*/
|
|
32175
|
+
async handleViewerCommand(cmd) {
|
|
32176
|
+
const { requestId } = cmd;
|
|
32177
|
+
if (!this.opts.onViewerCommand) {
|
|
32178
|
+
await this.sendViewerReply(requestId, {
|
|
32179
|
+
error: { message: "live viewer is not supported by this provider" }
|
|
32180
|
+
}).catch(() => {
|
|
32181
|
+
});
|
|
32182
|
+
return;
|
|
32183
|
+
}
|
|
32184
|
+
try {
|
|
32185
|
+
let input;
|
|
32186
|
+
if (cmd.input) {
|
|
32187
|
+
const decoded = Buffer.from(cmd.input, "base64").toString("utf-8");
|
|
32188
|
+
try {
|
|
32189
|
+
input = JSON.parse(decoded);
|
|
32190
|
+
} catch {
|
|
32191
|
+
input = void 0;
|
|
32192
|
+
}
|
|
32193
|
+
}
|
|
32194
|
+
const result = await this.opts.onViewerCommand({
|
|
32195
|
+
profileId: cmd.profileId,
|
|
32196
|
+
sessionId: cmd.sessionId,
|
|
32197
|
+
command: cmd.command,
|
|
32198
|
+
input,
|
|
32199
|
+
turn: cmd.turn
|
|
32200
|
+
});
|
|
32201
|
+
await this.sendViewerReply(requestId, { result });
|
|
32202
|
+
} catch (err) {
|
|
32203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32204
|
+
this.opts.log.warn(`[clip-provider] viewer command failed (profile=${cmd.profileId}, command=${cmd.command}): ${message}`);
|
|
32205
|
+
await this.sendViewerReply(requestId, { error: { message } }).catch(() => {
|
|
32206
|
+
});
|
|
32207
|
+
}
|
|
32208
|
+
}
|
|
32209
|
+
/** Send a hosted-viewer reply (base64-encoded JSON body) on the stream. */
|
|
32210
|
+
async sendViewerReply(requestId, reply) {
|
|
32211
|
+
const payload = Buffer.from(JSON.stringify(reply), "utf-8").toString("base64");
|
|
32212
|
+
await this.sendProviderMessage({ viewerResult: { requestId, payload } });
|
|
32213
|
+
}
|
|
31927
32214
|
// ---------------------------------------------------------------------------
|
|
31928
32215
|
// Sending
|
|
31929
32216
|
// ---------------------------------------------------------------------------
|
|
31930
|
-
async sendRegister() {
|
|
31931
|
-
const clips = this.buildClipRegistrations();
|
|
32217
|
+
async sendRegister(prebuilt) {
|
|
32218
|
+
const clips = prebuilt ?? this.buildClipRegistrations();
|
|
31932
32219
|
await this.sendProviderMessage({
|
|
31933
32220
|
register: {
|
|
31934
32221
|
providerName: this.opts.providerName,
|
|
31935
32222
|
clips
|
|
31936
32223
|
}
|
|
31937
32224
|
});
|
|
32225
|
+
this.lastRegisteredKey = registrationKey(clips);
|
|
31938
32226
|
}
|
|
31939
32227
|
async sendProviderMessage(msg) {
|
|
31940
32228
|
if (!this.stream || this.stream.closed || this.stream.destroyed) {
|
|
@@ -32001,13 +32289,619 @@ var ClipProvider = class _ClipProvider {
|
|
|
32001
32289
|
};
|
|
32002
32290
|
|
|
32003
32291
|
// ts/daemon/dist/clip-runtime/browser-profile-manager.js
|
|
32004
|
-
import { spawn as
|
|
32292
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
32005
32293
|
import { randomBytes } from "node:crypto";
|
|
32006
32294
|
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
32007
32295
|
import { createRequire } from "node:module";
|
|
32008
|
-
import * as net from "node:net";
|
|
32009
32296
|
import * as path9 from "node:path";
|
|
32010
32297
|
import { fileURLToPath } from "node:url";
|
|
32298
|
+
|
|
32299
|
+
// ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
|
|
32300
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
32301
|
+
|
|
32302
|
+
// ts/daemon/dist/clip-runtime/subprocess.js
|
|
32303
|
+
import * as net from "node:net";
|
|
32304
|
+
function formatErrorForLog(err) {
|
|
32305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32306
|
+
return message.replace(/\s+/g, " ").slice(0, 300);
|
|
32307
|
+
}
|
|
32308
|
+
async function findFreePort() {
|
|
32309
|
+
return new Promise((resolve8, reject) => {
|
|
32310
|
+
const server = net.createServer();
|
|
32311
|
+
server.unref();
|
|
32312
|
+
server.on("error", reject);
|
|
32313
|
+
server.listen(0, "127.0.0.1", () => {
|
|
32314
|
+
const address = server.address();
|
|
32315
|
+
server.close(() => {
|
|
32316
|
+
if (address && typeof address === "object") {
|
|
32317
|
+
resolve8(address.port);
|
|
32318
|
+
} else {
|
|
32319
|
+
reject(new Error("failed to allocate free port"));
|
|
32320
|
+
}
|
|
32321
|
+
});
|
|
32322
|
+
});
|
|
32323
|
+
});
|
|
32324
|
+
}
|
|
32325
|
+
function sleep(ms) {
|
|
32326
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
32327
|
+
}
|
|
32328
|
+
|
|
32329
|
+
// ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
|
|
32330
|
+
var BB_VIEWER_HEALTH_TIMEOUT_MS = 1e4;
|
|
32331
|
+
var BB_VIEWER_COMMAND_TIMEOUT_MS = 15e3;
|
|
32332
|
+
var BB_VIEWER_UNANSWERED_REAP_MS = 9e4;
|
|
32333
|
+
var BB_VIEWER_TERM_GRACE_MS = 5e3;
|
|
32334
|
+
function isLoopbackBindHost(host) {
|
|
32335
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
32336
|
+
}
|
|
32337
|
+
function kickHitsCurrentSession(requested, currentSessionId) {
|
|
32338
|
+
if (currentSessionId === void 0)
|
|
32339
|
+
return false;
|
|
32340
|
+
return requested === void 0 || requested === currentSessionId;
|
|
32341
|
+
}
|
|
32342
|
+
var BrowserViewerStreamer = class {
|
|
32343
|
+
host;
|
|
32344
|
+
// Live bb-viewer (WebRTC streamer) subprocess per profile. One per profile;
|
|
32345
|
+
// see StreamerState. Cleaned up on stream.close, stopProfile, resetProfile,
|
|
32346
|
+
// and stop().
|
|
32347
|
+
streamers = /* @__PURE__ */ new Map();
|
|
32348
|
+
// The most-recently server-side-kicked viewer session per profile (design §3.6
|
|
32349
|
+
// PR7). After a kick the streamer is gone, so the session-staleness guards
|
|
32350
|
+
// (which key off a live streamer) can no longer reject the kicked viewer's
|
|
32351
|
+
// straggler nav commands — this map does. Reset whenever the profile's streamer
|
|
32352
|
+
// is (re)killed via killProfileStreamer, so a fresh stream.start clears it. One
|
|
32353
|
+
// entry per profile (overwritten per kick); bounded, no leak.
|
|
32354
|
+
kickedSessions = /* @__PURE__ */ new Map();
|
|
32355
|
+
// Set by shutdown(): an in-flight spawnStreamer has no map entry yet, so a
|
|
32356
|
+
// shutdown during its health poll would otherwise let the bb-viewer child
|
|
32357
|
+
// survive daemon stop (on BYOC nothing else reaps it).
|
|
32358
|
+
stopping = false;
|
|
32359
|
+
constructor(host) {
|
|
32360
|
+
this.host = host;
|
|
32361
|
+
}
|
|
32362
|
+
/**
|
|
32363
|
+
* Handle a viewer control command for a profile. Throws on any failure (the
|
|
32364
|
+
* supervisor maps a throw to a `{error:{message}}` reply); never returns a
|
|
32365
|
+
* partial result. `data.input` / `data.turn` / `data.session_id` come from
|
|
32366
|
+
* MachineBrowserProfileViewerData.
|
|
32367
|
+
*/
|
|
32368
|
+
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
32369
|
+
if (!profileId)
|
|
32370
|
+
throw new Error("browser profile id is required");
|
|
32371
|
+
if (command !== "stream.start" && sessionId && this.kickedSessions.get(profileId) === sessionId) {
|
|
32372
|
+
throw new Error("viewer session was terminated");
|
|
32373
|
+
}
|
|
32374
|
+
switch (command) {
|
|
32375
|
+
case "stream.start":
|
|
32376
|
+
return this.viewerStreamStart(profileId, sessionId, turn);
|
|
32377
|
+
case "stream.answer":
|
|
32378
|
+
return this.viewerStreamAnswer(profileId, sessionId, input);
|
|
32379
|
+
case "stream.close":
|
|
32380
|
+
return this.viewerStreamClose(profileId, sessionId, input);
|
|
32381
|
+
case "stream.switch":
|
|
32382
|
+
return this.viewerStreamSwitch(profileId, sessionId, input);
|
|
32383
|
+
case "kick":
|
|
32384
|
+
return this.viewerKick(profileId, sessionId, input);
|
|
32385
|
+
case "close":
|
|
32386
|
+
return this.viewerCloseTab(profileId, sessionId, input);
|
|
32387
|
+
case "tab_list":
|
|
32388
|
+
case "tab_new":
|
|
32389
|
+
case "open":
|
|
32390
|
+
case "reload":
|
|
32391
|
+
case "back":
|
|
32392
|
+
case "forward":
|
|
32393
|
+
return this.viewerForwardNav(profileId, sessionId, command, input);
|
|
32394
|
+
default:
|
|
32395
|
+
throw new Error(`unknown viewer command: ${command}`);
|
|
32396
|
+
}
|
|
32397
|
+
}
|
|
32398
|
+
/**
|
|
32399
|
+
* kick — server-side per-viewer disconnect (design §3.6 PR7). Terminates ONE
|
|
32400
|
+
* viewer's WebRTC session: stop its bb-viewer streamer (killing the process
|
|
32401
|
+
* tears down the peer connection + datachannel) and reject the kicked session's
|
|
32402
|
+
* subsequent commands — while bb-browser + Chromium (the agent's live browser)
|
|
32403
|
+
* keep running. This is explicitly DISTINCT from stopping the pod / profile,
|
|
32404
|
+
* which would kill the agent's browser too; a viewer session is a sub-session of
|
|
32405
|
+
* the profile lease, so a kick does NOT release the lease.
|
|
32406
|
+
*
|
|
32407
|
+
* Targets `input.session_id` if given, else the current streamer's session.
|
|
32408
|
+
* Idempotent: kicking with no live streamer still records the kicked session.
|
|
32409
|
+
*/
|
|
32410
|
+
viewerKick(profileId, sessionId, input) {
|
|
32411
|
+
const streamer = this.streamers.get(profileId);
|
|
32412
|
+
const requested = typeof input?.session_id === "string" ? input.session_id : void 0;
|
|
32413
|
+
const target = requested || streamer?.sessionId || sessionId;
|
|
32414
|
+
const killsCurrent = kickHitsCurrentSession(requested, streamer?.sessionId);
|
|
32415
|
+
if (killsCurrent) {
|
|
32416
|
+
this.killProfileStreamer(profileId);
|
|
32417
|
+
}
|
|
32418
|
+
if (target)
|
|
32419
|
+
this.kickedSessions.set(profileId, target);
|
|
32420
|
+
this.host.log.info(`[bb-viewer] kicked viewer session ${target || "(none)"} for profile ${profileId}; pod + agent browser keep running`);
|
|
32421
|
+
return target ? { ok: true, kicked: true, session_id: target } : { ok: true, kicked: true };
|
|
32422
|
+
}
|
|
32423
|
+
/**
|
|
32424
|
+
* stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
|
|
32425
|
+
* one), resolve the profile's account-scoped page-target CDP ws URL, and run
|
|
32426
|
+
* the streamer's `connect` command. Records sessionId on the streamer entry.
|
|
32427
|
+
*/
|
|
32428
|
+
async viewerStreamStart(profileId, sessionId, turn) {
|
|
32429
|
+
this.killProfileStreamer(profileId);
|
|
32430
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer stream.start ${profileId}`);
|
|
32431
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32432
|
+
const streamer = await this.spawnStreamer(profileId, sessionId, turn);
|
|
32433
|
+
try {
|
|
32434
|
+
const result = await this.streamerCommand(streamer, "connect", { cdpUrl });
|
|
32435
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32436
|
+
const reapTimer = setTimeout(() => {
|
|
32437
|
+
const current = this.streamers.get(profileId);
|
|
32438
|
+
if (current === streamer && !current.answered) {
|
|
32439
|
+
this.host.log.warn(`[bb-viewer] no stream.answer for ${profileId} within ${BB_VIEWER_UNANSWERED_REAP_MS / 1e3}s; reaping abandoned streamer`);
|
|
32440
|
+
this.killProfileStreamer(profileId);
|
|
32441
|
+
}
|
|
32442
|
+
}, BB_VIEWER_UNANSWERED_REAP_MS);
|
|
32443
|
+
reapTimer.unref?.();
|
|
32444
|
+
return { ...result, streamed_tab_id: streamer.streamedTabId };
|
|
32445
|
+
} catch (err) {
|
|
32446
|
+
this.killProfileStreamer(profileId);
|
|
32447
|
+
throw err;
|
|
32448
|
+
}
|
|
32449
|
+
}
|
|
32450
|
+
/**
|
|
32451
|
+
* stream.answer — apply the client's WebRTC answer to the profile's streamer.
|
|
32452
|
+
* Rejects if the streamer's sessionId no longer matches (a stale viewer
|
|
32453
|
+
* session from a superseded stream.start).
|
|
32454
|
+
*/
|
|
32455
|
+
async viewerStreamAnswer(profileId, sessionId, input) {
|
|
32456
|
+
const streamer = this.streamers.get(profileId);
|
|
32457
|
+
if (!streamer)
|
|
32458
|
+
throw new Error("no active viewer streamer for profile");
|
|
32459
|
+
if (streamer.sessionId !== sessionId)
|
|
32460
|
+
throw new Error("stale viewer session");
|
|
32461
|
+
streamer.answered = true;
|
|
32462
|
+
return this.streamerCommand(streamer, "answer", input ?? {});
|
|
32463
|
+
}
|
|
32464
|
+
/**
|
|
32465
|
+
* stream.close — best-effort stop the streamer, then kill + remove it. A
|
|
32466
|
+
* stale session's close (independent HTTP legs can arrive out of order after
|
|
32467
|
+
* a retry) must NOT tear down a successor session's live streamer, so a
|
|
32468
|
+
* mismatched sessionId is ignored. An EMPTY sessionId gets the same stale
|
|
32469
|
+
* treatment — a client closed mid-handshake never received a session id, and
|
|
32470
|
+
* its unscoped close racing the next session's stream.start would kill that
|
|
32471
|
+
* session's live streamer (React StrictMode's dev double-mount exercises
|
|
32472
|
+
* exactly this ordering). The only unscoped close is an explicit
|
|
32473
|
+
* `{force:true}` input, reserved for a server-initiated admin disconnect.
|
|
32474
|
+
*/
|
|
32475
|
+
async viewerStreamClose(profileId, sessionId, input) {
|
|
32476
|
+
const streamer = this.streamers.get(profileId);
|
|
32477
|
+
if (streamer) {
|
|
32478
|
+
const force = input?.force === true;
|
|
32479
|
+
if (!force && streamer.sessionId !== sessionId) {
|
|
32480
|
+
return { ok: true, stale: true };
|
|
32481
|
+
}
|
|
32482
|
+
try {
|
|
32483
|
+
await this.streamerCommand(streamer, "stop", {});
|
|
32484
|
+
} catch (err) {
|
|
32485
|
+
this.host.log.warn(`[bb-viewer] stop command failed for ${profileId} (killing anyway): ${formatErrorForLog(err)}`);
|
|
32486
|
+
}
|
|
32487
|
+
this.killProfileStreamer(profileId);
|
|
32488
|
+
}
|
|
32489
|
+
return { ok: true };
|
|
32490
|
+
}
|
|
32491
|
+
/**
|
|
32492
|
+
* stream.switch — point the existing streamer at a different account-scoped
|
|
32493
|
+
* tab. input {tab} is the bb-browser short tab id / target id. Guarded by
|
|
32494
|
+
* sessionId like stream.answer: a superseded session must not steer the
|
|
32495
|
+
* current streamer, and an EMPTY sessionId is rejected the same way — every
|
|
32496
|
+
* real caller has its session id by the time it can switch (it arrives with
|
|
32497
|
+
* the stream.start response), so a session-less switch is by definition not
|
|
32498
|
+
* the current viewer.
|
|
32499
|
+
*/
|
|
32500
|
+
async viewerStreamSwitch(profileId, sessionId, input) {
|
|
32501
|
+
const streamer = this.streamers.get(profileId);
|
|
32502
|
+
if (!streamer)
|
|
32503
|
+
throw new Error("no active viewer streamer for profile");
|
|
32504
|
+
if (!sessionId || streamer.sessionId !== sessionId)
|
|
32505
|
+
throw new Error("stale viewer session");
|
|
32506
|
+
const tab = input?.tab;
|
|
32507
|
+
if (tab === void 0 || tab === null || tab === "") {
|
|
32508
|
+
throw new Error("stream.switch requires input.tab");
|
|
32509
|
+
}
|
|
32510
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId, tab), `viewer stream.switch ${profileId}`);
|
|
32511
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32512
|
+
const result = await this.streamerCommand(streamer, "switch", { cdpUrl });
|
|
32513
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32514
|
+
return { ...result, streamed_tab_id: streamer.streamedTabId };
|
|
32515
|
+
}
|
|
32516
|
+
/**
|
|
32517
|
+
* close — close an account tab via bb-browser, then, if the closed tab was
|
|
32518
|
+
* the one being streamed, re-point the streamer at the profile's next active
|
|
32519
|
+
* tab. Without the re-switch the streamer stays bound to a destroyed CDP
|
|
32520
|
+
* target and the viewer goes black. Returns `streamed_tab_id` when a
|
|
32521
|
+
* re-switch happened so the client can adopt the new active tab.
|
|
32522
|
+
*/
|
|
32523
|
+
async viewerCloseTab(profileId, sessionId, input) {
|
|
32524
|
+
const ref = viewerTabRef(input ?? {});
|
|
32525
|
+
const closedTargetId = ref !== void 0 ? await this.accountTabTargetId(profileId, ref) : await this.accountActivePageTargetId(profileId);
|
|
32526
|
+
const result = await this.viewerForwardNav(profileId, sessionId, "close", {
|
|
32527
|
+
...input ?? {},
|
|
32528
|
+
tabId: closedTargetId
|
|
32529
|
+
});
|
|
32530
|
+
const streamer = this.streamers.get(profileId);
|
|
32531
|
+
if (!streamer || !streamer.streamedTabId || streamer.streamedTabId !== closedTargetId) {
|
|
32532
|
+
return result;
|
|
32533
|
+
}
|
|
32534
|
+
const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer close re-switch ${profileId}`);
|
|
32535
|
+
await this.bringTargetToFront(cdpUrl);
|
|
32536
|
+
const sw = await this.streamerCommand(streamer, "switch", { cdpUrl });
|
|
32537
|
+
streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
|
|
32538
|
+
return { ...result, ...sw, streamed_tab_id: streamer.streamedTabId };
|
|
32539
|
+
}
|
|
32540
|
+
/**
|
|
32541
|
+
* Bring a CDP page target to the foreground via Chrome's DevTools HTTP
|
|
32542
|
+
* endpoint (`/json/activate/{targetId}`, same host:port as the CDP ws). Chrome
|
|
32543
|
+
* does not composite backgrounded tabs, so `Page.startScreencast` on a
|
|
32544
|
+
* background tab yields zero frames (black viewer). Best-effort: a failure
|
|
32545
|
+
* must not block the stream. host:port + targetId are parsed from the cdpUrl
|
|
32546
|
+
* (`ws://host:port/devtools/page/<targetId>`) to avoid an extra /status call.
|
|
32547
|
+
*/
|
|
32548
|
+
async bringTargetToFront(cdpUrl) {
|
|
32549
|
+
const m = /^ws:\/\/([^/]+)\/devtools\/page\/(.+)$/.exec(cdpUrl);
|
|
32550
|
+
if (!m)
|
|
32551
|
+
return;
|
|
32552
|
+
const [, hostPort, targetId] = m;
|
|
32553
|
+
try {
|
|
32554
|
+
const resp = await fetch(`http://${hostPort}/json/activate/${targetId}`, {
|
|
32555
|
+
method: "GET",
|
|
32556
|
+
signal: AbortSignal.timeout(5e3)
|
|
32557
|
+
});
|
|
32558
|
+
if (!resp.ok) {
|
|
32559
|
+
this.host.log.warn(`[bb-viewer] activate target ${targetId} returned ${resp.status} (continuing)`);
|
|
32560
|
+
}
|
|
32561
|
+
} catch (err) {
|
|
32562
|
+
this.host.log.warn(`[bb-viewer] activate target ${targetId} failed (continuing): ${formatErrorForLog(err)}`);
|
|
32563
|
+
}
|
|
32564
|
+
}
|
|
32565
|
+
/**
|
|
32566
|
+
* Forward a navigation command (tab_list/tab_new/open/reload/back/forward/
|
|
32567
|
+
* close) to bb-browser scoped to the profile's account. This lets the human
|
|
32568
|
+
* in the live viewer navigate and handle OAuth popups directly. Reuses the
|
|
32569
|
+
* same account-scoped sendCommand path the agent invokes use.
|
|
32570
|
+
*
|
|
32571
|
+
* SECURITY — the `account` field alone does NOT scope tab addressing:
|
|
32572
|
+
* bb-browser's ensurePageTarget resolves a client-supplied tab/tabId against
|
|
32573
|
+
* EVERY page target in the shared Chrome (all accounts), and tab_list
|
|
32574
|
+
* returns every tab with `account` as a mere annotation (verified in
|
|
32575
|
+
* @pinixai/bb-browser-pro@0.15.0 dist/daemon.js ensurePageTarget/tab_list).
|
|
32576
|
+
* On a multi-profile host that would let one profile's viewer list,
|
|
32577
|
+
* navigate, and close other profiles' logged-in tabs. So before forwarding:
|
|
32578
|
+
* (a) any tab ref must resolve through accountTabTargetId, which throws on
|
|
32579
|
+
* tabs the profile's account does not own; (b) ref-less tab-addressed
|
|
32580
|
+
* commands are pinned to the profile's own active tab instead of
|
|
32581
|
+
* bb-browser's account-blind global current tab; (c) tab_list output is
|
|
32582
|
+
* filtered to the profile's own rows; and (d) open/tab_new URLs must be
|
|
32583
|
+
* http(s)/about:blank — file:// or chrome:// would read the host
|
|
32584
|
+
* filesystem / browser internals straight into the video stream.
|
|
32585
|
+
*/
|
|
32586
|
+
async viewerForwardNav(profileId, sessionId, command, input) {
|
|
32587
|
+
if (command !== "tab_list") {
|
|
32588
|
+
const streamer = this.streamers.get(profileId);
|
|
32589
|
+
if (streamer && streamer.sessionId !== sessionId) {
|
|
32590
|
+
throw new Error("stale viewer session");
|
|
32591
|
+
}
|
|
32592
|
+
}
|
|
32593
|
+
return this.host.withDaemonRecovery(async () => {
|
|
32594
|
+
await this.host.ensureAccount(profileId);
|
|
32595
|
+
const request3 = { ...input ?? {} };
|
|
32596
|
+
if (command === "open" || command === "tab_new") {
|
|
32597
|
+
assertViewerNavigableUrl(request3.url);
|
|
32598
|
+
}
|
|
32599
|
+
const ref = viewerTabRef(request3);
|
|
32600
|
+
if (ref !== void 0) {
|
|
32601
|
+
request3.tabId = await this.accountTabTargetId(profileId, ref);
|
|
32602
|
+
delete request3.tab;
|
|
32603
|
+
} else if (REFLESS_PIN_NAV.has(command)) {
|
|
32604
|
+
request3.tabId = await this.accountActivePageTargetId(profileId);
|
|
32605
|
+
}
|
|
32606
|
+
const result = await this.host.sendBrowserCommand({
|
|
32607
|
+
...request3,
|
|
32608
|
+
method: command,
|
|
32609
|
+
account: profileId
|
|
32610
|
+
});
|
|
32611
|
+
return command === "tab_list" ? filterTabListToAccount(result, profileId) : result;
|
|
32612
|
+
}, `viewer ${command} ${profileId}`);
|
|
32613
|
+
}
|
|
32614
|
+
/**
|
|
32615
|
+
* Resolve `ws://<cdpHost>:<cdpPort>/devtools/page/<targetId>` for a PAGE
|
|
32616
|
+
* target owned by THIS profile's account — NOT bb-browser's global current
|
|
32617
|
+
* tab (which `getCurrentTabCdpUrl`/`getTabCdpUrl` in the bundled daemon use
|
|
32618
|
+
* via `cdp.currentTargetId`, an account-blind global; see daemon.js
|
|
32619
|
+
* line ~10389/10410). When `tabRef` is given (stream.switch) we resolve that
|
|
32620
|
+
* specific account tab; otherwise the profile's active page tab.
|
|
32621
|
+
*
|
|
32622
|
+
* Evidence from the installed @pinixai/bb-browser-pro@0.15.0 dist/daemon.js:
|
|
32623
|
+
* - GET /status (handleStatus, line ~1659-1668) returns `cdpHost` + `cdpPort`
|
|
32624
|
+
* — the CDP host/port the daemon's Chrome listens on.
|
|
32625
|
+
* - The CDP page ws URL format is `ws://<cdp.host>:<cdp.port>/devtools/page/<page.id>`
|
|
32626
|
+
* (getCurrentTabCdpUrl, line ~10392), where `page.id` is the full CDP
|
|
32627
|
+
* targetId (getTargets maps `t.targetId` → `.id`, line ~2461-2466).
|
|
32628
|
+
* - tab_list (command handler, line ~1041-1059) returns per-tab
|
|
32629
|
+
* `{ tabId: t.id, tab: <shortId>, account: <accountName>, url, ... }`, so
|
|
32630
|
+
* `tabId` is exactly the full targetId for the devtools URL and `account`
|
|
32631
|
+
* is the per-tab attribution we filter on (reused by findAccountTabOnHost).
|
|
32632
|
+
*/
|
|
32633
|
+
async resolveAccountPageCdpUrl(profileId, tabRef) {
|
|
32634
|
+
await this.host.ensureAccount(profileId);
|
|
32635
|
+
const { host, port } = await this.host.cdpEndpoint();
|
|
32636
|
+
const targetId = tabRef !== void 0 ? await this.accountTabTargetId(profileId, tabRef) : await this.accountActivePageTargetId(profileId);
|
|
32637
|
+
return `ws://${host}:${port}/devtools/page/${targetId}`;
|
|
32638
|
+
}
|
|
32639
|
+
/**
|
|
32640
|
+
* Full CDP targetId of the profile account's active page tab (the owned tab
|
|
32641
|
+
* Chrome marks active, else the first owned tab). If the account owns no
|
|
32642
|
+
* page tab yet, open `about:blank` for it first (mirrors the agent-invoke
|
|
32643
|
+
* path's "always give a fresh account an owned tab" rule).
|
|
32644
|
+
*/
|
|
32645
|
+
async accountActivePageTargetId(profileId) {
|
|
32646
|
+
let targetId = await this.firstAccountPageTargetId(profileId);
|
|
32647
|
+
if (targetId !== void 0)
|
|
32648
|
+
return targetId;
|
|
32649
|
+
await this.host.sendBrowserCommand({
|
|
32650
|
+
method: "tab_new",
|
|
32651
|
+
account: profileId,
|
|
32652
|
+
url: "about:blank"
|
|
32653
|
+
});
|
|
32654
|
+
targetId = await this.firstAccountPageTargetId(profileId);
|
|
32655
|
+
if (targetId === void 0) {
|
|
32656
|
+
throw new Error(`no page tab found for profile account ${profileId}`);
|
|
32657
|
+
}
|
|
32658
|
+
return targetId;
|
|
32659
|
+
}
|
|
32660
|
+
/**
|
|
32661
|
+
* Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
|
|
32662
|
+
* Chrome marks `active` when it belongs to this account, else the first owned
|
|
32663
|
+
* tab (the global active flag is account-blind — another profile's tab may
|
|
32664
|
+
* hold it, which must not leak here).
|
|
32665
|
+
*/
|
|
32666
|
+
async firstAccountPageTargetId(profileId) {
|
|
32667
|
+
const tabs = await this.accountTabs(profileId);
|
|
32668
|
+
let firstOwned;
|
|
32669
|
+
for (const t of tabs) {
|
|
32670
|
+
if (t.account !== profileId)
|
|
32671
|
+
continue;
|
|
32672
|
+
const tabId = t.tabId;
|
|
32673
|
+
if (typeof tabId !== "string" || !tabId)
|
|
32674
|
+
continue;
|
|
32675
|
+
if (t.active === true)
|
|
32676
|
+
return tabId;
|
|
32677
|
+
firstOwned ??= tabId;
|
|
32678
|
+
}
|
|
32679
|
+
return firstOwned;
|
|
32680
|
+
}
|
|
32681
|
+
/**
|
|
32682
|
+
* Full CDP targetId for a specific account tab, addressed by the bb-browser
|
|
32683
|
+
* short tab id (`tab`) or full target id (`tabId`). Used by stream.switch.
|
|
32684
|
+
*/
|
|
32685
|
+
async accountTabTargetId(profileId, tabRef) {
|
|
32686
|
+
const ref = String(tabRef);
|
|
32687
|
+
const tabs = await this.accountTabs(profileId);
|
|
32688
|
+
for (const t of tabs) {
|
|
32689
|
+
if (t.account !== profileId)
|
|
32690
|
+
continue;
|
|
32691
|
+
const tabId = typeof t.tabId === "string" ? t.tabId : void 0;
|
|
32692
|
+
const shortTab = t.tab === void 0 ? void 0 : String(t.tab);
|
|
32693
|
+
if (tabId && tabId === ref || shortTab && shortTab === ref) {
|
|
32694
|
+
if (!tabId)
|
|
32695
|
+
break;
|
|
32696
|
+
return tabId;
|
|
32697
|
+
}
|
|
32698
|
+
}
|
|
32699
|
+
throw new Error(`tab ${ref} not found for profile account ${profileId}`);
|
|
32700
|
+
}
|
|
32701
|
+
/** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
|
|
32702
|
+
async accountTabs(profileId) {
|
|
32703
|
+
const list = await this.host.sendBrowserCommand({ method: "tab_list", account: profileId });
|
|
32704
|
+
return Array.isArray(list.tabs) ? list.tabs : [];
|
|
32705
|
+
}
|
|
32706
|
+
// ---- bb-viewer streamer lifecycle ----
|
|
32707
|
+
/**
|
|
32708
|
+
* Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
|
|
32709
|
+
* is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
|
|
32710
|
+
* defaulting to `bb-viewer` on PATH — we NEVER download it. A free port is
|
|
32711
|
+
* allocated per streamer. Stores the entry in `this.streamers` once healthy.
|
|
32712
|
+
*/
|
|
32713
|
+
async spawnStreamer(profileId, sessionId, turn) {
|
|
32714
|
+
if (this.stopping)
|
|
32715
|
+
throw new Error("viewer streamer is shutting down");
|
|
32716
|
+
const bin = process.env.PRLL_BB_VIEWER_BIN ?? "bb-viewer";
|
|
32717
|
+
const port = await findFreePort();
|
|
32718
|
+
const args = ["--api-only", "--port", String(port)];
|
|
32719
|
+
const bindHost = process.env.PRLL_BB_VIEWER_HOST?.trim();
|
|
32720
|
+
if (bindHost) {
|
|
32721
|
+
if (!isLoopbackBindHost(bindHost)) {
|
|
32722
|
+
throw new Error(`PRLL_BB_VIEWER_HOST must be loopback (127.0.0.1, localhost, ::1, [::1]) \u2014 refusing to bind bb-viewer's unauthenticated /command to ${bindHost}`);
|
|
32723
|
+
}
|
|
32724
|
+
args.push("--host", bindHost);
|
|
32725
|
+
}
|
|
32726
|
+
if (turn?.url) {
|
|
32727
|
+
args.push("--turn-url", turn.url, "--turn-user", turn.username ?? "", "--turn-cred", turn.credential ?? "");
|
|
32728
|
+
}
|
|
32729
|
+
const streamerEnv = {};
|
|
32730
|
+
for (const key of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "DISPLAY"]) {
|
|
32731
|
+
const val = process.env[key];
|
|
32732
|
+
if (val !== void 0)
|
|
32733
|
+
streamerEnv[key] = val;
|
|
32734
|
+
}
|
|
32735
|
+
this.host.log.info(`[bb-viewer] spawning for profile ${profileId} on port ${port}`);
|
|
32736
|
+
const child = spawn3(bin, args, { stdio: ["ignore", "pipe", "pipe"], env: streamerEnv });
|
|
32737
|
+
let childError = null;
|
|
32738
|
+
child.once("error", (err) => {
|
|
32739
|
+
childError = err;
|
|
32740
|
+
});
|
|
32741
|
+
child.once("exit", () => {
|
|
32742
|
+
const current = this.streamers.get(profileId);
|
|
32743
|
+
if (current?.child === child) {
|
|
32744
|
+
this.streamers.delete(profileId);
|
|
32745
|
+
this.host.log.warn(`[bb-viewer] streamer for ${profileId} exited; cleared stale entry`);
|
|
32746
|
+
}
|
|
32747
|
+
});
|
|
32748
|
+
child.stdout?.on("data", (chunk) => {
|
|
32749
|
+
const text = chunk.toString("utf8").trim();
|
|
32750
|
+
if (text)
|
|
32751
|
+
this.host.log.info(`[bb-viewer] ${text}`);
|
|
32752
|
+
});
|
|
32753
|
+
child.stderr?.on("data", (chunk) => {
|
|
32754
|
+
const text = chunk.toString("utf8").trim();
|
|
32755
|
+
if (text)
|
|
32756
|
+
this.host.log.warn(`[bb-viewer] ${text}`);
|
|
32757
|
+
});
|
|
32758
|
+
const deadline = Date.now() + BB_VIEWER_HEALTH_TIMEOUT_MS;
|
|
32759
|
+
while (Date.now() < deadline) {
|
|
32760
|
+
if (this.stopping) {
|
|
32761
|
+
this.killChild(child, "streamer for stopping daemon");
|
|
32762
|
+
throw new Error("viewer streamer is shutting down");
|
|
32763
|
+
}
|
|
32764
|
+
if (childError) {
|
|
32765
|
+
throw childError;
|
|
32766
|
+
}
|
|
32767
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
32768
|
+
throw new Error("bb-viewer exited during startup");
|
|
32769
|
+
}
|
|
32770
|
+
try {
|
|
32771
|
+
const resp = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
32772
|
+
signal: AbortSignal.timeout(2e3)
|
|
32773
|
+
});
|
|
32774
|
+
if (resp.ok) {
|
|
32775
|
+
const streamer = { child, port, sessionId };
|
|
32776
|
+
this.streamers.set(profileId, streamer);
|
|
32777
|
+
this.host.log.info(`[bb-viewer] ready for profile ${profileId} on port ${port}`);
|
|
32778
|
+
return streamer;
|
|
32779
|
+
}
|
|
32780
|
+
} catch {
|
|
32781
|
+
}
|
|
32782
|
+
await sleep(200);
|
|
32783
|
+
}
|
|
32784
|
+
this.killChild(child, "unhealthy streamer");
|
|
32785
|
+
throw new Error("bb-viewer did not become healthy in time");
|
|
32786
|
+
}
|
|
32787
|
+
/**
|
|
32788
|
+
* SIGTERM with a SIGKILL escalation: a bb-viewer that hangs or ignores TERM
|
|
32789
|
+
* would otherwise outlive its teardown holding the port + CDP session (the
|
|
32790
|
+
* shell-level pattern-kill only exists on the hosted pod, not BYOC). The
|
|
32791
|
+
* escalation timer is unref'd so it never holds the daemon open; it is
|
|
32792
|
+
* cleared on exit. Caveat: a daemon process that exits immediately after
|
|
32793
|
+
* shutdown() abandons the timer — acceptable, the TERM was still sent.
|
|
32794
|
+
*/
|
|
32795
|
+
killChild(child, label) {
|
|
32796
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
32797
|
+
return;
|
|
32798
|
+
try {
|
|
32799
|
+
child.kill("SIGTERM");
|
|
32800
|
+
} catch {
|
|
32801
|
+
return;
|
|
32802
|
+
}
|
|
32803
|
+
const killTimer = setTimeout(() => {
|
|
32804
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
32805
|
+
this.host.log.warn(`[bb-viewer] ${label} ignored SIGTERM; escalating to SIGKILL`);
|
|
32806
|
+
try {
|
|
32807
|
+
child.kill("SIGKILL");
|
|
32808
|
+
} catch {
|
|
32809
|
+
}
|
|
32810
|
+
}
|
|
32811
|
+
}, BB_VIEWER_TERM_GRACE_MS);
|
|
32812
|
+
killTimer.unref?.();
|
|
32813
|
+
child.once("exit", () => clearTimeout(killTimer));
|
|
32814
|
+
}
|
|
32815
|
+
/**
|
|
32816
|
+
* POST a command to the profile's bb-viewer `/command` endpoint. Parses the
|
|
32817
|
+
* `{result}` / `{error:{message}}` envelope (bb-viewer api.go) and returns
|
|
32818
|
+
* the inner result, throwing the error message on failure.
|
|
32819
|
+
*/
|
|
32820
|
+
async streamerCommand(streamer, method, params) {
|
|
32821
|
+
const controller = new AbortController();
|
|
32822
|
+
const timer = setTimeout(() => controller.abort(), BB_VIEWER_COMMAND_TIMEOUT_MS);
|
|
32823
|
+
try {
|
|
32824
|
+
const resp = await fetch(`http://127.0.0.1:${streamer.port}/command`, {
|
|
32825
|
+
method: "POST",
|
|
32826
|
+
headers: { "Content-Type": "application/json" },
|
|
32827
|
+
body: JSON.stringify({ method, params }),
|
|
32828
|
+
signal: controller.signal
|
|
32829
|
+
});
|
|
32830
|
+
const text = await resp.text();
|
|
32831
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
32832
|
+
if (parsed.error) {
|
|
32833
|
+
throw new Error(parsed.error.message || `bb-viewer ${method} failed`);
|
|
32834
|
+
}
|
|
32835
|
+
if (!resp.ok) {
|
|
32836
|
+
throw new Error(`bb-viewer ${method} returned ${resp.status}: ${text}`);
|
|
32837
|
+
}
|
|
32838
|
+
return parsed.result ?? {};
|
|
32839
|
+
} finally {
|
|
32840
|
+
clearTimeout(timer);
|
|
32841
|
+
}
|
|
32842
|
+
}
|
|
32843
|
+
/**
|
|
32844
|
+
* Kill + remove the profile's bb-viewer streamer, if any. Idempotent. Also
|
|
32845
|
+
* clears any kick marker for the profile: a (re)kill establishes a clean
|
|
32846
|
+
* streamer slot, so a subsequent stream.start starts un-kicked. viewerKick
|
|
32847
|
+
* re-records the kicked session AFTER calling this.
|
|
32848
|
+
*/
|
|
32849
|
+
killProfileStreamer(profileId) {
|
|
32850
|
+
this.kickedSessions.delete(profileId);
|
|
32851
|
+
const streamer = this.streamers.get(profileId);
|
|
32852
|
+
if (!streamer)
|
|
32853
|
+
return;
|
|
32854
|
+
this.streamers.delete(profileId);
|
|
32855
|
+
this.killChild(streamer.child, `streamer for ${profileId}`);
|
|
32856
|
+
}
|
|
32857
|
+
/**
|
|
32858
|
+
* Tear down every profile streamer and refuse new spawns (daemon stop).
|
|
32859
|
+
* Idempotent.
|
|
32860
|
+
*/
|
|
32861
|
+
shutdown() {
|
|
32862
|
+
this.stopping = true;
|
|
32863
|
+
for (const profileId of [...this.streamers.keys()]) {
|
|
32864
|
+
this.killProfileStreamer(profileId);
|
|
32865
|
+
}
|
|
32866
|
+
}
|
|
32867
|
+
};
|
|
32868
|
+
function targetIdFromCdpUrl(cdpUrl) {
|
|
32869
|
+
return /\/devtools\/page\/(.+)$/.exec(cdpUrl)?.[1];
|
|
32870
|
+
}
|
|
32871
|
+
var REFLESS_PIN_NAV = /* @__PURE__ */ new Set(["reload", "back", "forward", "close"]);
|
|
32872
|
+
function viewerTabRef(request3) {
|
|
32873
|
+
for (const key of ["tabId", "tab"]) {
|
|
32874
|
+
const v = request3[key];
|
|
32875
|
+
if (typeof v === "string" && v !== "" || typeof v === "number")
|
|
32876
|
+
return String(v);
|
|
32877
|
+
}
|
|
32878
|
+
return void 0;
|
|
32879
|
+
}
|
|
32880
|
+
var VIEWER_NAVIGABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
32881
|
+
function assertViewerNavigableUrl(raw) {
|
|
32882
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
32883
|
+
return;
|
|
32884
|
+
if (typeof raw !== "string")
|
|
32885
|
+
throw new Error("url must be a string");
|
|
32886
|
+
if (raw === "about:blank")
|
|
32887
|
+
return;
|
|
32888
|
+
let parsed;
|
|
32889
|
+
try {
|
|
32890
|
+
parsed = new URL(raw);
|
|
32891
|
+
} catch {
|
|
32892
|
+
throw new Error(`invalid url: ${raw}`);
|
|
32893
|
+
}
|
|
32894
|
+
if (!VIEWER_NAVIGABLE_PROTOCOLS.has(parsed.protocol)) {
|
|
32895
|
+
throw new Error(`url scheme not allowed in the live viewer: ${parsed.protocol.replace(/:$/, "")}`);
|
|
32896
|
+
}
|
|
32897
|
+
}
|
|
32898
|
+
function filterTabListToAccount(result, account) {
|
|
32899
|
+
const tabs = Array.isArray(result.tabs) ? result.tabs : [];
|
|
32900
|
+
const owned = tabs.filter((t) => t?.account === account);
|
|
32901
|
+
return { ...result, tabs: owned, activeIndex: owned.findIndex((t) => t?.active === true) };
|
|
32902
|
+
}
|
|
32903
|
+
|
|
32904
|
+
// ts/daemon/dist/clip-runtime/browser-profile-manager.js
|
|
32011
32905
|
var BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15e3;
|
|
32012
32906
|
var BrowserCommandError = class extends Error {
|
|
32013
32907
|
method;
|
|
@@ -32027,9 +32921,20 @@ var BrowserProfileManager = class {
|
|
|
32027
32921
|
ensuredAccounts = /* @__PURE__ */ new Set();
|
|
32028
32922
|
ensuringAccounts = /* @__PURE__ */ new Map();
|
|
32029
32923
|
reportedStatuses = /* @__PURE__ */ new Map();
|
|
32924
|
+
// Live viewer orchestration (bb-viewer streamer subprocesses + viewer nav)
|
|
32925
|
+
// lives in BrowserViewerStreamer; the manager hands it the account-scoped
|
|
32926
|
+
// bb-browser command path via the host interface.
|
|
32927
|
+
viewer;
|
|
32030
32928
|
constructor(opts) {
|
|
32031
32929
|
this.opts = opts;
|
|
32032
32930
|
mkdirSync5(this.opts.homeDir, { recursive: true });
|
|
32931
|
+
this.viewer = new BrowserViewerStreamer({
|
|
32932
|
+
log: this.opts.log,
|
|
32933
|
+
sendBrowserCommand: (request3) => this.sendCommand(request3),
|
|
32934
|
+
ensureAccount: (account) => this.ensureAccount(account),
|
|
32935
|
+
withDaemonRecovery: (operation, label) => this.withDaemonRecovery(operation, label),
|
|
32936
|
+
cdpEndpoint: () => this.cdpEndpoint()
|
|
32937
|
+
});
|
|
32033
32938
|
}
|
|
32034
32939
|
async invoke({ profileId, account, command, input }) {
|
|
32035
32940
|
if (!profileId)
|
|
@@ -32096,12 +33001,14 @@ var BrowserProfileManager = class {
|
|
|
32096
33001
|
if (!profileId)
|
|
32097
33002
|
throw new Error("browser profile id is required");
|
|
32098
33003
|
this.reportedStatuses.delete(profileId);
|
|
33004
|
+
this.viewer.killProfileStreamer(profileId);
|
|
32099
33005
|
this.reportStatus(profileId, "stopped");
|
|
32100
33006
|
}
|
|
32101
33007
|
async resetProfile(profileId) {
|
|
32102
33008
|
if (!profileId)
|
|
32103
33009
|
throw new Error("browser profile id is required");
|
|
32104
33010
|
this.reportedStatuses.delete(profileId);
|
|
33011
|
+
this.viewer.killProfileStreamer(profileId);
|
|
32105
33012
|
try {
|
|
32106
33013
|
await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
|
|
32107
33014
|
this.reportStatus(profileId, "stopped");
|
|
@@ -32112,6 +33019,7 @@ var BrowserProfileManager = class {
|
|
|
32112
33019
|
}
|
|
32113
33020
|
}
|
|
32114
33021
|
async stop() {
|
|
33022
|
+
this.viewer.shutdown();
|
|
32115
33023
|
const daemon = this.daemon;
|
|
32116
33024
|
this.daemon = null;
|
|
32117
33025
|
this.starting = null;
|
|
@@ -32128,6 +33036,25 @@ var BrowserProfileManager = class {
|
|
|
32128
33036
|
daemon.child.kill("SIGTERM");
|
|
32129
33037
|
}
|
|
32130
33038
|
}
|
|
33039
|
+
/**
|
|
33040
|
+
* Viewer control command entrypoint — delegates to BrowserViewerStreamer
|
|
33041
|
+
* (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
|
|
33042
|
+
* subprocesses and viewer tab navigation.
|
|
33043
|
+
*/
|
|
33044
|
+
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
33045
|
+
return this.viewer.handleViewerCommand(profileId, sessionId, command, input, turn);
|
|
33046
|
+
}
|
|
33047
|
+
/** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
|
|
33048
|
+
async cdpEndpoint() {
|
|
33049
|
+
const daemon = await this.ensureDaemon();
|
|
33050
|
+
const status = await this.post("/status", daemon, void 0, 5e3);
|
|
33051
|
+
const host = status.cdpHost?.trim();
|
|
33052
|
+
const port = status.cdpPort;
|
|
33053
|
+
if (!host || typeof port !== "number" || !Number.isFinite(port)) {
|
|
33054
|
+
throw new Error("bb-browser /status did not report a CDP endpoint");
|
|
33055
|
+
}
|
|
33056
|
+
return { host, port };
|
|
33057
|
+
}
|
|
32131
33058
|
async prepareOpenProfile(profileId, url) {
|
|
32132
33059
|
const accountCreated = await this.ensureAccount(profileId, url !== "about:blank" ? url : void 0);
|
|
32133
33060
|
await this.sendCommand({ method: "tab_list", account: profileId });
|
|
@@ -32386,7 +33313,7 @@ var BrowserProfileManager = class {
|
|
|
32386
33313
|
const port = await findFreePort();
|
|
32387
33314
|
const token = randomToken();
|
|
32388
33315
|
const daemonPath = resolveBbBrowserDaemonPath();
|
|
32389
|
-
const child =
|
|
33316
|
+
const child = spawn4(process.execPath, [daemonPath, "--host", host, "--port", String(port), "--token", token], {
|
|
32390
33317
|
env: {
|
|
32391
33318
|
...process.env,
|
|
32392
33319
|
BB_BROWSER_HOME: this.opts.homeDir
|
|
@@ -32500,10 +33427,6 @@ function comparableUrl(url) {
|
|
|
32500
33427
|
return null;
|
|
32501
33428
|
}
|
|
32502
33429
|
}
|
|
32503
|
-
function formatErrorForLog(err) {
|
|
32504
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
32505
|
-
return message.replace(/\s+/g, " ").slice(0, 300);
|
|
32506
|
-
}
|
|
32507
33430
|
function resolveBbBrowserDaemonPath() {
|
|
32508
33431
|
const sibling = path9.join(path9.dirname(fileURLToPath(import.meta.url)), "bb-browser-daemon.js");
|
|
32509
33432
|
if (existsSync8(sibling))
|
|
@@ -32518,29 +33441,9 @@ function resolveBbBrowserDaemonPath() {
|
|
|
32518
33441
|
}
|
|
32519
33442
|
return full;
|
|
32520
33443
|
}
|
|
32521
|
-
async function findFreePort() {
|
|
32522
|
-
return new Promise((resolve8, reject) => {
|
|
32523
|
-
const server = net.createServer();
|
|
32524
|
-
server.unref();
|
|
32525
|
-
server.on("error", reject);
|
|
32526
|
-
server.listen(0, "127.0.0.1", () => {
|
|
32527
|
-
const address = server.address();
|
|
32528
|
-
server.close(() => {
|
|
32529
|
-
if (address && typeof address === "object") {
|
|
32530
|
-
resolve8(address.port);
|
|
32531
|
-
} else {
|
|
32532
|
-
reject(new Error("failed to allocate free port"));
|
|
32533
|
-
}
|
|
32534
|
-
});
|
|
32535
|
-
});
|
|
32536
|
-
});
|
|
32537
|
-
}
|
|
32538
33444
|
function randomToken() {
|
|
32539
33445
|
return randomBytes(16).toString("hex");
|
|
32540
33446
|
}
|
|
32541
|
-
function sleep(ms) {
|
|
32542
|
-
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
32543
|
-
}
|
|
32544
33447
|
|
|
32545
33448
|
// ts/daemon/dist/supervisor.js
|
|
32546
33449
|
init_config();
|
|
@@ -32940,7 +33843,7 @@ function shellQuote(value) {
|
|
|
32940
33843
|
}
|
|
32941
33844
|
|
|
32942
33845
|
// ts/daemon/dist/workspace.js
|
|
32943
|
-
import { spawn as
|
|
33846
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
32944
33847
|
import { createHash as createHash3 } from "node:crypto";
|
|
32945
33848
|
import * as fs11 from "node:fs";
|
|
32946
33849
|
import * as path12 from "node:path";
|
|
@@ -33244,7 +34147,7 @@ function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
|
|
|
33244
34147
|
clearTimeout(killTimer);
|
|
33245
34148
|
fn();
|
|
33246
34149
|
};
|
|
33247
|
-
const child =
|
|
34150
|
+
const child = spawn5(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
33248
34151
|
timeoutTimer = setTimeout(() => {
|
|
33249
34152
|
timedOut = true;
|
|
33250
34153
|
child.kill("SIGTERM");
|
|
@@ -33397,7 +34300,12 @@ var DaemonSupervisor = class {
|
|
|
33397
34300
|
children = /* @__PURE__ */ new Map();
|
|
33398
34301
|
spawningAgents = /* @__PURE__ */ new Set();
|
|
33399
34302
|
pendingWorkspaceSetup = /* @__PURE__ */ new Set();
|
|
33400
|
-
|
|
34303
|
+
// ONE queue per profile for BOTH lifecycle (open/stop/reset) and viewer
|
|
34304
|
+
// (stream.*/nav) commands. They mutate the same streamer + account state, so
|
|
34305
|
+
// two independent queues would let a stop/reset interleave with an in-flight
|
|
34306
|
+
// stream.start (the kill fires before the streamer registers and misses it,
|
|
34307
|
+
// leaving a live streamer on a profile the server just marked stopped).
|
|
34308
|
+
browserProfileOpQueues = /* @__PURE__ */ new Map();
|
|
33401
34309
|
workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
|
|
33402
34310
|
cancelledSpawns = /* @__PURE__ */ new Set();
|
|
33403
34311
|
ws = null;
|
|
@@ -33558,6 +34466,10 @@ var DaemonSupervisor = class {
|
|
|
33558
34466
|
this.log.info(`WS: browser profile lifecycle requested \u2014 profile=${data.profile_id} action=${data.action}`);
|
|
33559
34467
|
void this.enqueueBrowserProfileLifecycle(data);
|
|
33560
34468
|
});
|
|
34469
|
+
this.ws.on("machine.browser_profile.viewer", (data) => {
|
|
34470
|
+
this.log.info(`WS: browser profile viewer command \u2014 profile=${data.profile_id} command=${data.command}`);
|
|
34471
|
+
this.enqueueBrowserProfileViewer(data);
|
|
34472
|
+
});
|
|
33561
34473
|
this.ws.onStateChange((state) => {
|
|
33562
34474
|
if (state === "disconnected" || state === "reconnecting") {
|
|
33563
34475
|
this.log.warn(`machine WS state: ${state}`);
|
|
@@ -33707,10 +34619,13 @@ var DaemonSupervisor = class {
|
|
|
33707
34619
|
for (const profile of profiles) {
|
|
33708
34620
|
if (profile.status !== "running" && profile.status !== "pending")
|
|
33709
34621
|
continue;
|
|
34622
|
+
if (!profile.machine_id)
|
|
34623
|
+
continue;
|
|
34624
|
+
const machineId = profile.machine_id;
|
|
33710
34625
|
try {
|
|
33711
34626
|
if (profile.status === "pending") {
|
|
33712
34627
|
await this.enqueueBrowserProfileLifecycle({
|
|
33713
|
-
machine_id:
|
|
34628
|
+
machine_id: machineId,
|
|
33714
34629
|
profile_id: profile.id,
|
|
33715
34630
|
action: "open"
|
|
33716
34631
|
});
|
|
@@ -33869,17 +34784,45 @@ var DaemonSupervisor = class {
|
|
|
33869
34784
|
}
|
|
33870
34785
|
}
|
|
33871
34786
|
enqueueBrowserProfileLifecycle(data) {
|
|
33872
|
-
|
|
34787
|
+
return this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileLifecycle(data));
|
|
34788
|
+
}
|
|
34789
|
+
enqueueBrowserProfileOp(profileId, op) {
|
|
34790
|
+
const previous = this.browserProfileOpQueues.get(profileId) ?? Promise.resolve();
|
|
33873
34791
|
const next = previous.catch(() => {
|
|
33874
|
-
}).then(
|
|
33875
|
-
this.
|
|
34792
|
+
}).then(op);
|
|
34793
|
+
this.browserProfileOpQueues.set(profileId, next);
|
|
33876
34794
|
void next.finally(() => {
|
|
33877
|
-
if (this.
|
|
33878
|
-
this.
|
|
34795
|
+
if (this.browserProfileOpQueues.get(profileId) === next) {
|
|
34796
|
+
this.browserProfileOpQueues.delete(profileId);
|
|
33879
34797
|
}
|
|
33880
34798
|
});
|
|
33881
34799
|
return next;
|
|
33882
34800
|
}
|
|
34801
|
+
/**
|
|
34802
|
+
* Handle one viewer control command and reply via REST. Always answers the
|
|
34803
|
+
* request/reply bridge exactly once ({result} on success, {error:{message}}
|
|
34804
|
+
* on failure) and never throws into the WS event loop — mirrors
|
|
34805
|
+
* handleFilesystemBrowse. Serialized per profile by enqueueBrowserProfileViewer.
|
|
34806
|
+
*/
|
|
34807
|
+
async handleBrowserProfileViewer(data) {
|
|
34808
|
+
if (!this.browserProfileManager) {
|
|
34809
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, {
|
|
34810
|
+
error: { message: "Browser profile runtime is disabled on this daemon" }
|
|
34811
|
+
}).catch((err) => this.log.warn(`browser profile viewer negative ACK failed: ${String(err)}`));
|
|
34812
|
+
return;
|
|
34813
|
+
}
|
|
34814
|
+
try {
|
|
34815
|
+
const result = await this.browserProfileManager.handleViewerCommand(data.profile_id, data.session_id, data.command, data.input, data.turn);
|
|
34816
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, { result });
|
|
34817
|
+
} catch (err) {
|
|
34818
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
34819
|
+
this.log.warn(`browser profile viewer command failed (profile=${data.profile_id}, command=${data.command}): ${message}`);
|
|
34820
|
+
await this.client.postBrowserProfileViewerResponse(data.request_id, { error: { message } }).catch((replyErr) => this.log.warn(`browser profile viewer error ACK failed: ${String(replyErr)}`));
|
|
34821
|
+
}
|
|
34822
|
+
}
|
|
34823
|
+
enqueueBrowserProfileViewer(data) {
|
|
34824
|
+
void this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileViewer(data));
|
|
34825
|
+
}
|
|
33883
34826
|
async handleClipSync(_data) {
|
|
33884
34827
|
await this.reconcileMachineClips();
|
|
33885
34828
|
}
|
|
@@ -33938,7 +34881,7 @@ var DaemonSupervisor = class {
|
|
|
33938
34881
|
}
|
|
33939
34882
|
if (changed) {
|
|
33940
34883
|
this.log.info(`machine clips synced \u2014 count=${desired.length}`);
|
|
33941
|
-
|
|
34884
|
+
this.clipProvider?.reregister();
|
|
33942
34885
|
}
|
|
33943
34886
|
}
|
|
33944
34887
|
startClipReconcileTimer() {
|
|
@@ -34067,15 +35010,6 @@ var DaemonSupervisor = class {
|
|
|
34067
35010
|
}
|
|
34068
35011
|
return null;
|
|
34069
35012
|
}
|
|
34070
|
-
async reconnectClipProvider() {
|
|
34071
|
-
if (!this.clipProvider)
|
|
34072
|
-
return;
|
|
34073
|
-
const provider = this.clipProvider;
|
|
34074
|
-
this.clipProvider = null;
|
|
34075
|
-
this.connectedClipServiceUrl = null;
|
|
34076
|
-
await provider.disconnect().catch((err) => this.log.warn(`clip provider reconnect disconnect failed: ${String(err)}`));
|
|
34077
|
-
await this.applyClipProviderState();
|
|
34078
|
-
}
|
|
34079
35013
|
async handleWorkspaceSetupRequested(agentId) {
|
|
34080
35014
|
if (this.spawningAgents.has(agentId)) {
|
|
34081
35015
|
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|
|
@@ -34356,7 +35290,7 @@ var DaemonSupervisor = class {
|
|
|
34356
35290
|
const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
|
|
34357
35291
|
const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(" ")}` : adapter.bin;
|
|
34358
35292
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
|
|
34359
|
-
const child =
|
|
35293
|
+
const child = spawn6(adapter.bin, adapter.args, {
|
|
34360
35294
|
env,
|
|
34361
35295
|
stdio: ["ignore", "inherit", "inherit"],
|
|
34362
35296
|
detached: false
|