@parall/daemon 1.35.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 +409 -12
- package/bundle/parall-codex-agent.js +409 -12
- package/bundle/parall-daemon.js +299 -29
- 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-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 +35 -1
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +97 -1
- 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/supervisor.d.ts +0 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +13 -13
- package/package.json +11 -8
package/bundle/parall-daemon.js
CHANGED
|
@@ -27256,6 +27256,17 @@ var ENDPOINTS = {
|
|
|
27256
27256
|
SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
|
|
27257
27257
|
SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
|
|
27258
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}`,
|
|
27259
27270
|
// Invitations (org-scoped, admin)
|
|
27260
27271
|
ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
|
|
27261
27272
|
ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
|
|
@@ -27465,6 +27476,7 @@ var WS_EVENTS = {
|
|
|
27465
27476
|
// ts/sdk/dist/client.js
|
|
27466
27477
|
var ParallClient = class _ParallClient {
|
|
27467
27478
|
baseUrl;
|
|
27479
|
+
wikiBaseUrl;
|
|
27468
27480
|
token;
|
|
27469
27481
|
onTokenExpired;
|
|
27470
27482
|
getRefreshToken;
|
|
@@ -27516,12 +27528,22 @@ var ParallClient = class _ParallClient {
|
|
|
27516
27528
|
}
|
|
27517
27529
|
constructor(options = {}) {
|
|
27518
27530
|
this.baseUrl = options.baseUrl ?? "";
|
|
27531
|
+
this.wikiBaseUrl = options.wikiBaseUrl ?? this.baseUrl;
|
|
27519
27532
|
this.token = options.token ?? null;
|
|
27520
27533
|
this.onTokenExpired = options.onTokenExpired;
|
|
27521
27534
|
this.getRefreshToken = options.getRefreshToken;
|
|
27522
27535
|
this.setTokens = options.setTokens;
|
|
27523
27536
|
this.swimlaneName = options.swimlaneName;
|
|
27524
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
|
+
}
|
|
27525
27547
|
setToken(token) {
|
|
27526
27548
|
this.token = token;
|
|
27527
27549
|
}
|
|
@@ -27586,7 +27608,7 @@ var ParallClient = class _ParallClient {
|
|
|
27586
27608
|
if (!retried) {
|
|
27587
27609
|
await this.ensureFreshToken(path15);
|
|
27588
27610
|
}
|
|
27589
|
-
let url = `${this.
|
|
27611
|
+
let url = `${this.baseUrlFor(path15)}${path15}`;
|
|
27590
27612
|
if (query) {
|
|
27591
27613
|
const params = new URLSearchParams();
|
|
27592
27614
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -27658,7 +27680,7 @@ var ParallClient = class _ParallClient {
|
|
|
27658
27680
|
void _drop;
|
|
27659
27681
|
let res;
|
|
27660
27682
|
try {
|
|
27661
|
-
res = await fetch(`${this.
|
|
27683
|
+
res = await fetch(`${this.baseUrlFor(path15)}${path15}`, {
|
|
27662
27684
|
method,
|
|
27663
27685
|
headers,
|
|
27664
27686
|
body,
|
|
@@ -27818,6 +27840,21 @@ var ParallClient = class _ParallClient {
|
|
|
27818
27840
|
q.limit = String(params.limit);
|
|
27819
27841
|
return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
|
|
27820
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
|
+
}
|
|
27821
27858
|
// ---- Invitations ----
|
|
27822
27859
|
async createInvitation(orgId, email, role) {
|
|
27823
27860
|
return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
|
|
@@ -28407,7 +28444,7 @@ var ParallClient = class _ParallClient {
|
|
|
28407
28444
|
* Returns null when the server responds with 304 (config unchanged).
|
|
28408
28445
|
*/
|
|
28409
28446
|
async getPlatformConfig(currentVersion) {
|
|
28410
|
-
const url = `${this.
|
|
28447
|
+
const url = `${this.baseUrlFor(ENDPOINTS.PLATFORM_CONFIG)}${ENDPOINTS.PLATFORM_CONFIG}`;
|
|
28411
28448
|
const extra = {};
|
|
28412
28449
|
if (currentVersion !== void 0) {
|
|
28413
28450
|
extra["If-None-Match"] = currentVersion;
|
|
@@ -28573,6 +28610,55 @@ var ParallClient = class _ParallClient {
|
|
|
28573
28610
|
async getScheduleRun(orgId, runId) {
|
|
28574
28611
|
return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
|
|
28575
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
|
+
}
|
|
28576
28662
|
// ---- Wikis (org-scoped) ----
|
|
28577
28663
|
async createWiki(orgId, data) {
|
|
28578
28664
|
return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
|
|
@@ -28587,8 +28673,35 @@ var ParallClient = class _ParallClient {
|
|
|
28587
28673
|
async getWikiTree(orgId, wikiId, params) {
|
|
28588
28674
|
return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
|
|
28589
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
|
+
}
|
|
28590
28700
|
async getWikiBlob(orgId, wikiId, params) {
|
|
28591
|
-
|
|
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;
|
|
28592
28705
|
}
|
|
28593
28706
|
async getWikiNodeSections(orgId, wikiId, params) {
|
|
28594
28707
|
return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
|
|
@@ -28678,7 +28791,10 @@ var ParallClient = class _ParallClient {
|
|
|
28678
28791
|
* token — don't leak it.
|
|
28679
28792
|
*/
|
|
28680
28793
|
async getWikiFilePreviewUrl(orgId, wikiId, params) {
|
|
28681
|
-
|
|
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;
|
|
28682
28798
|
}
|
|
28683
28799
|
// ---- Wiki Path Scopes (AFCS ACL) ----
|
|
28684
28800
|
async getWikiPathScopes(orgId, wikiId) {
|
|
@@ -28894,6 +29010,9 @@ var ParallClient = class _ParallClient {
|
|
|
28894
29010
|
const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
|
|
28895
29011
|
return resp.data;
|
|
28896
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. */
|
|
28897
29016
|
async listBrowserProfiles(orgId) {
|
|
28898
29017
|
const resp = await this.request("GET", ENDPOINTS.BROWSER_PROFILES(orgId));
|
|
28899
29018
|
return resp.data;
|
|
@@ -31692,6 +31811,10 @@ function encodeEnvelope2(msg) {
|
|
|
31692
31811
|
header.writeUInt32BE(json.length, 1);
|
|
31693
31812
|
return Buffer.concat([header, json]);
|
|
31694
31813
|
}
|
|
31814
|
+
function registrationKey(clips) {
|
|
31815
|
+
const sorted = [...clips].sort((a, b) => (a.alias ?? "").localeCompare(b.alias ?? ""));
|
|
31816
|
+
return JSON.stringify(sorted);
|
|
31817
|
+
}
|
|
31695
31818
|
var EnvelopeDecoder2 = class _EnvelopeDecoder {
|
|
31696
31819
|
buf = Buffer.alloc(0);
|
|
31697
31820
|
push(chunk) {
|
|
@@ -31727,7 +31850,15 @@ var ClipProvider = class _ClipProvider {
|
|
|
31727
31850
|
heartbeatTimer = null;
|
|
31728
31851
|
statusUnsubscribe = null;
|
|
31729
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).
|
|
31730
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;
|
|
31731
31862
|
// Monotonic stream id, bumped by openStream(). Handlers from a stream that a
|
|
31732
31863
|
// newer openStream() has superseded bail in handleDisconnect(gen), so their
|
|
31733
31864
|
// late close/end/error can't reconnect against the healthy replacement.
|
|
@@ -31768,6 +31899,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31768
31899
|
if (this.stopped)
|
|
31769
31900
|
return;
|
|
31770
31901
|
const gen = ++this.streamGeneration;
|
|
31902
|
+
this.lastRegisteredKey = null;
|
|
31771
31903
|
try {
|
|
31772
31904
|
this.closeStream();
|
|
31773
31905
|
this.closeSession();
|
|
@@ -31866,21 +31998,57 @@ var ClipProvider = class _ClipProvider {
|
|
|
31866
31998
|
startHeartbeat() {
|
|
31867
31999
|
this.clearHeartbeatTimer();
|
|
31868
32000
|
this.heartbeatTimer = setInterval(() => {
|
|
31869
|
-
|
|
31870
|
-
this.needsReregister = false;
|
|
31871
|
-
this.opts.log.info("[clip-provider] manifest updated \u2014 reconnecting to re-register clips");
|
|
31872
|
-
this.connected = false;
|
|
31873
|
-
this.clearHeartbeatTimer();
|
|
31874
|
-
this.clearReconnectTimer();
|
|
31875
|
-
void this.openStream();
|
|
31876
|
-
return;
|
|
31877
|
-
}
|
|
32001
|
+
this.flushReregister();
|
|
31878
32002
|
this.sendProviderMessage({ ping: { sentAtUnixMs: Date.now() } }).catch((err) => {
|
|
31879
32003
|
this.opts.log.warn(`[clip-provider] heartbeat send failed: ${String(err)}`);
|
|
31880
32004
|
});
|
|
31881
32005
|
}, _ClipProvider.HEARTBEAT_INTERVAL_MS);
|
|
31882
32006
|
this.heartbeatTimer.unref?.();
|
|
31883
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
|
+
}
|
|
31884
32052
|
closeStream() {
|
|
31885
32053
|
if (this.stream) {
|
|
31886
32054
|
try {
|
|
@@ -31915,7 +32083,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31915
32083
|
});
|
|
31916
32084
|
});
|
|
31917
32085
|
this.manifestUnsubscribe = mgr.addManifestListener(() => {
|
|
31918
|
-
this.
|
|
32086
|
+
this.reregister();
|
|
31919
32087
|
});
|
|
31920
32088
|
}
|
|
31921
32089
|
unsubscribeStatusChanges() {
|
|
@@ -31932,6 +32100,8 @@ var ClipProvider = class _ClipProvider {
|
|
|
31932
32100
|
this.handleRegistered(msg.registerResponse);
|
|
31933
32101
|
} else if (msg.invokeCommand) {
|
|
31934
32102
|
void this.handleInvokeCommand(msg.invokeCommand);
|
|
32103
|
+
} else if (msg.viewerCommand) {
|
|
32104
|
+
void this.handleViewerCommand(msg.viewerCommand);
|
|
31935
32105
|
} else if (msg.pong !== void 0) {
|
|
31936
32106
|
}
|
|
31937
32107
|
}
|
|
@@ -31941,6 +32111,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31941
32111
|
this.connected = true;
|
|
31942
32112
|
this.reconnectAttempt = 0;
|
|
31943
32113
|
this.startHeartbeat();
|
|
32114
|
+
this.flushReregister();
|
|
31944
32115
|
} else {
|
|
31945
32116
|
this.opts.log.error(`[clip-provider] registration rejected: ${reg.message}`);
|
|
31946
32117
|
this.stopped = true;
|
|
@@ -31994,17 +32165,64 @@ var ClipProvider = class _ClipProvider {
|
|
|
31994
32165
|
});
|
|
31995
32166
|
}
|
|
31996
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
|
+
}
|
|
31997
32214
|
// ---------------------------------------------------------------------------
|
|
31998
32215
|
// Sending
|
|
31999
32216
|
// ---------------------------------------------------------------------------
|
|
32000
|
-
async sendRegister() {
|
|
32001
|
-
const clips = this.buildClipRegistrations();
|
|
32217
|
+
async sendRegister(prebuilt) {
|
|
32218
|
+
const clips = prebuilt ?? this.buildClipRegistrations();
|
|
32002
32219
|
await this.sendProviderMessage({
|
|
32003
32220
|
register: {
|
|
32004
32221
|
providerName: this.opts.providerName,
|
|
32005
32222
|
clips
|
|
32006
32223
|
}
|
|
32007
32224
|
});
|
|
32225
|
+
this.lastRegisteredKey = registrationKey(clips);
|
|
32008
32226
|
}
|
|
32009
32227
|
async sendProviderMessage(msg) {
|
|
32010
32228
|
if (!this.stream || this.stream.closed || this.stream.destroyed) {
|
|
@@ -32113,12 +32331,27 @@ var BB_VIEWER_HEALTH_TIMEOUT_MS = 1e4;
|
|
|
32113
32331
|
var BB_VIEWER_COMMAND_TIMEOUT_MS = 15e3;
|
|
32114
32332
|
var BB_VIEWER_UNANSWERED_REAP_MS = 9e4;
|
|
32115
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
|
+
}
|
|
32116
32342
|
var BrowserViewerStreamer = class {
|
|
32117
32343
|
host;
|
|
32118
32344
|
// Live bb-viewer (WebRTC streamer) subprocess per profile. One per profile;
|
|
32119
32345
|
// see StreamerState. Cleaned up on stream.close, stopProfile, resetProfile,
|
|
32120
32346
|
// and stop().
|
|
32121
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();
|
|
32122
32355
|
// Set by shutdown(): an in-flight spawnStreamer has no map entry yet, so a
|
|
32123
32356
|
// shutdown during its health poll would otherwise let the bb-viewer child
|
|
32124
32357
|
// survive daemon stop (on BYOC nothing else reaps it).
|
|
@@ -32135,6 +32368,9 @@ var BrowserViewerStreamer = class {
|
|
|
32135
32368
|
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
32136
32369
|
if (!profileId)
|
|
32137
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
|
+
}
|
|
32138
32374
|
switch (command) {
|
|
32139
32375
|
case "stream.start":
|
|
32140
32376
|
return this.viewerStreamStart(profileId, sessionId, turn);
|
|
@@ -32144,6 +32380,8 @@ var BrowserViewerStreamer = class {
|
|
|
32144
32380
|
return this.viewerStreamClose(profileId, sessionId, input);
|
|
32145
32381
|
case "stream.switch":
|
|
32146
32382
|
return this.viewerStreamSwitch(profileId, sessionId, input);
|
|
32383
|
+
case "kick":
|
|
32384
|
+
return this.viewerKick(profileId, sessionId, input);
|
|
32147
32385
|
case "close":
|
|
32148
32386
|
return this.viewerCloseTab(profileId, sessionId, input);
|
|
32149
32387
|
case "tab_list":
|
|
@@ -32157,6 +32395,31 @@ var BrowserViewerStreamer = class {
|
|
|
32157
32395
|
throw new Error(`unknown viewer command: ${command}`);
|
|
32158
32396
|
}
|
|
32159
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
|
+
}
|
|
32160
32423
|
/**
|
|
32161
32424
|
* stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
|
|
32162
32425
|
* one), resolve the profile's account-scoped page-target CDP ws URL, and run
|
|
@@ -32453,6 +32716,13 @@ var BrowserViewerStreamer = class {
|
|
|
32453
32716
|
const bin = process.env.PRLL_BB_VIEWER_BIN ?? "bb-viewer";
|
|
32454
32717
|
const port = await findFreePort();
|
|
32455
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
|
+
}
|
|
32456
32726
|
if (turn?.url) {
|
|
32457
32727
|
args.push("--turn-url", turn.url, "--turn-user", turn.username ?? "", "--turn-cred", turn.credential ?? "");
|
|
32458
32728
|
}
|
|
@@ -32570,8 +32840,14 @@ var BrowserViewerStreamer = class {
|
|
|
32570
32840
|
clearTimeout(timer);
|
|
32571
32841
|
}
|
|
32572
32842
|
}
|
|
32573
|
-
/**
|
|
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
|
+
*/
|
|
32574
32849
|
killProfileStreamer(profileId) {
|
|
32850
|
+
this.kickedSessions.delete(profileId);
|
|
32575
32851
|
const streamer = this.streamers.get(profileId);
|
|
32576
32852
|
if (!streamer)
|
|
32577
32853
|
return;
|
|
@@ -34343,10 +34619,13 @@ var DaemonSupervisor = class {
|
|
|
34343
34619
|
for (const profile of profiles) {
|
|
34344
34620
|
if (profile.status !== "running" && profile.status !== "pending")
|
|
34345
34621
|
continue;
|
|
34622
|
+
if (!profile.machine_id)
|
|
34623
|
+
continue;
|
|
34624
|
+
const machineId = profile.machine_id;
|
|
34346
34625
|
try {
|
|
34347
34626
|
if (profile.status === "pending") {
|
|
34348
34627
|
await this.enqueueBrowserProfileLifecycle({
|
|
34349
|
-
machine_id:
|
|
34628
|
+
machine_id: machineId,
|
|
34350
34629
|
profile_id: profile.id,
|
|
34351
34630
|
action: "open"
|
|
34352
34631
|
});
|
|
@@ -34602,7 +34881,7 @@ var DaemonSupervisor = class {
|
|
|
34602
34881
|
}
|
|
34603
34882
|
if (changed) {
|
|
34604
34883
|
this.log.info(`machine clips synced \u2014 count=${desired.length}`);
|
|
34605
|
-
|
|
34884
|
+
this.clipProvider?.reregister();
|
|
34606
34885
|
}
|
|
34607
34886
|
}
|
|
34608
34887
|
startClipReconcileTimer() {
|
|
@@ -34731,15 +35010,6 @@ var DaemonSupervisor = class {
|
|
|
34731
35010
|
}
|
|
34732
35011
|
return null;
|
|
34733
35012
|
}
|
|
34734
|
-
async reconnectClipProvider() {
|
|
34735
|
-
if (!this.clipProvider)
|
|
34736
|
-
return;
|
|
34737
|
-
const provider = this.clipProvider;
|
|
34738
|
-
this.clipProvider = null;
|
|
34739
|
-
this.connectedClipServiceUrl = null;
|
|
34740
|
-
await provider.disconnect().catch((err) => this.log.warn(`clip provider reconnect disconnect failed: ${String(err)}`));
|
|
34741
|
-
await this.applyClipProviderState();
|
|
34742
|
-
}
|
|
34743
35013
|
async handleWorkspaceSetupRequested(agentId) {
|
|
34744
35014
|
if (this.spawningAgents.has(agentId)) {
|
|
34745
35015
|
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|