@parall/daemon 1.32.1 → 1.34.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/bb-browser-daemon.js +15628 -0
- package/bundle/buildDomTree.js +1501 -0
- package/bundle/manifest.json +19 -11
- package/bundle/parall-claude-agent.js +185 -25
- package/bundle/parall-codex-agent.js +185 -25
- package/bundle/parall-daemon.js +4647 -2856
- package/bundle/parall-openclaw-agent.js +4 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/clip-runtime/browser-dependency.d.ts +20 -0
- package/dist/clip-runtime/browser-dependency.d.ts.map +1 -0
- package/dist/clip-runtime/browser-dependency.js +52 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +70 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -0
- package/dist/clip-runtime/browser-profile-manager.js +608 -0
- package/dist/clip-runtime/clip-provider.d.ts +10 -1
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +63 -21
- package/dist/clip-runtime/hub-client.d.ts +79 -0
- package/dist/clip-runtime/hub-client.d.ts.map +1 -0
- package/dist/clip-runtime/hub-client.js +320 -0
- package/dist/clip-runtime/index.d.ts +2 -0
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/clip-runtime/index.js +2 -0
- package/dist/clip-runtime/ipc.d.ts +6 -0
- package/dist/clip-runtime/ipc.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.d.ts +16 -8
- package/dist/clip-runtime/manifest.d.ts.map +1 -1
- package/dist/clip-runtime/manifest.js +13 -0
- package/dist/clip-runtime/process-manager.d.ts +55 -3
- package/dist/clip-runtime/process-manager.d.ts.map +1 -1
- package/dist/clip-runtime/process-manager.js +226 -48
- package/dist/clip-runtime/process.d.ts +15 -1
- package/dist/clip-runtime/process.d.ts.map +1 -1
- package/dist/clip-runtime/process.js +73 -10
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/index.js +46 -5
- package/dist/runtime-bin-resolver.d.ts +7 -0
- package/dist/runtime-bin-resolver.d.ts.map +1 -0
- package/dist/runtime-bin-resolver.js +292 -0
- package/dist/supervisor.d.ts +53 -4
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +449 -117
- package/package.json +7 -6
package/dist/supervisor.js
CHANGED
|
@@ -3,18 +3,21 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { effectiveLLMSourceExplicit } from '@parall/agent-core';
|
|
5
5
|
import { ParallWs, } from '@parall/sdk';
|
|
6
|
-
import {
|
|
6
|
+
import { installClip, parseSource } from './clip-runtime/clip-installer.js';
|
|
7
|
+
import { parseIpcCommands } from './clip-runtime/manifest.js';
|
|
8
|
+
import { BrowserProfileManager, ClipProcessManager, ClipProvider, HubClient, } from './clip-runtime/index.js';
|
|
7
9
|
import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from './config.js';
|
|
10
|
+
import { listDirectory } from './filesystem.js';
|
|
8
11
|
import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
|
|
12
|
+
import { applyRuntimeBinaryEnv } from './runtime-bin-resolver.js';
|
|
9
13
|
import { prepareWorkspace } from './workspace.js';
|
|
10
|
-
import { ClipProcessManager, ClipProvider } from './clip-runtime/index.js';
|
|
11
|
-
import { installClip, parseSource } from './clip-runtime/clip-installer.js';
|
|
12
14
|
const RUNTIME_PACKAGES = {
|
|
13
15
|
'claude-code': '@parall/claude-agent',
|
|
14
16
|
codex: '@parall/codex-agent',
|
|
15
17
|
openclaw: '@parall/openclaw-agent',
|
|
16
18
|
};
|
|
17
19
|
const WORKSPACE_SETUP_RETRY_DELAY_MS = 5_000;
|
|
20
|
+
const CLIP_RECONCILE_INTERVAL_MS = 5 * 60_000;
|
|
18
21
|
/**
|
|
19
22
|
* Sleep that wakes early on abort. Returns true if the full delay elapsed,
|
|
20
23
|
* false if aborted. Used by bootstrap retry and the outer keepalive in
|
|
@@ -54,6 +57,7 @@ export class DaemonSupervisor {
|
|
|
54
57
|
children = new Map();
|
|
55
58
|
spawningAgents = new Set();
|
|
56
59
|
pendingWorkspaceSetup = new Set();
|
|
60
|
+
browserProfileLifecycleQueues = new Map();
|
|
57
61
|
workspaceSetupRetryTimers = new Map();
|
|
58
62
|
cancelledSpawns = new Set();
|
|
59
63
|
ws = null;
|
|
@@ -61,11 +65,29 @@ export class DaemonSupervisor {
|
|
|
61
65
|
machineId = null;
|
|
62
66
|
machineOrgId = null;
|
|
63
67
|
machineLlmSource = 'parall';
|
|
68
|
+
// Whether this machine contributes its local clips as a hub provider. The
|
|
69
|
+
// org admin toggles it via PATCH /machines/{id}/provider-enabled; default true.
|
|
70
|
+
machineProviderEnabled = true;
|
|
71
|
+
// Externally-reachable ProviderStream endpoint delivered by the server via
|
|
72
|
+
// GET /machines/me (grey-cloud clip-rpc host). Null for hosted machines or
|
|
73
|
+
// pre-field servers; applyClipProviderState falls back to config.apiUrl.
|
|
74
|
+
machineClipProviderUrl = null;
|
|
75
|
+
// The serviceUrl the live clipProvider is currently connected to — lets
|
|
76
|
+
// applyClipProviderState detect an endpoint change and reconnect.
|
|
77
|
+
connectedClipServiceUrl = null;
|
|
64
78
|
stopResolve = null;
|
|
65
79
|
updater = null;
|
|
66
80
|
healthConfirmed = false;
|
|
81
|
+
browserProfileManager = null;
|
|
67
82
|
clipManager = null;
|
|
68
83
|
clipProvider = null;
|
|
84
|
+
clipReconcileTimer = null;
|
|
85
|
+
clipReconcileInFlight = null;
|
|
86
|
+
// Execution-side hub client for cross-machine dependency resolution
|
|
87
|
+
// (GetBindings + Invoke). Cached by resolved endpoint so a clip_provider_url
|
|
88
|
+
// rollout transparently rebuilds it against the new grey-cloud host.
|
|
89
|
+
hubClient = null;
|
|
90
|
+
hubClientUrl = null;
|
|
69
91
|
constructor(config, client, log) {
|
|
70
92
|
this.config = config;
|
|
71
93
|
this.client = client;
|
|
@@ -98,39 +120,41 @@ export class DaemonSupervisor {
|
|
|
98
120
|
this.migrateFlatLayout();
|
|
99
121
|
// Initialize Clip runtime manager (optional, for Pinix Clip subprocess management)
|
|
100
122
|
if (process.env.PRLL_CLIP_RUNTIME_ENABLED === 'true') {
|
|
123
|
+
this.browserProfileManager = new BrowserProfileManager({
|
|
124
|
+
homeDir: path.join(this.config.rootStateDir, 'bb-browser'),
|
|
125
|
+
log: this.log,
|
|
126
|
+
reportStatus: (profileId, status, errorMsg) => {
|
|
127
|
+
this.client
|
|
128
|
+
.reportBrowserProfileStatus(profileId, status, errorMsg)
|
|
129
|
+
.catch((err) => this.log.warn(`browser profile status report failed: ${String(err)}`));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
101
132
|
this.clipManager = new ClipProcessManager({
|
|
102
133
|
clipsDir: path.join(this.config.rootStateDir, 'clips'),
|
|
103
134
|
dataDir: path.join(this.config.rootStateDir, 'clip-data'),
|
|
135
|
+
browserProfileManager: this.browserProfileManager,
|
|
136
|
+
// Execution side: nested browser dependency invokes resolve their
|
|
137
|
+
// binding and route through the hub (no local shortcut).
|
|
138
|
+
hubClient: () => this.getHubClient(),
|
|
139
|
+
ensureInstalled: (config) => this.ensureClipInstalled(config),
|
|
104
140
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
// Connect to Clip Service as a Provider when URL is configured
|
|
112
|
-
const clipServiceUrl = process.env.PRLL_CLIP_SERVICE_URL?.trim();
|
|
113
|
-
if (clipServiceUrl && this.machineOrgId && this.machineId) {
|
|
114
|
-
this.clipProvider = new ClipProvider({
|
|
115
|
-
serviceUrl: clipServiceUrl,
|
|
116
|
-
authKey: this.config.apiKey,
|
|
117
|
-
orgId: this.machineOrgId,
|
|
118
|
-
providerName: `daemon-${this.machineId}`,
|
|
119
|
-
clipManager: this.clipManager,
|
|
120
|
-
log: this.log,
|
|
121
|
-
});
|
|
122
|
-
this.clipProvider
|
|
123
|
-
.connect()
|
|
124
|
-
.catch((err) => this.log.warn(`clip provider connect failed: ${String(err)}`));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// Report daemon version via heartbeat (best-effort)
|
|
128
|
-
const daemonVersion = this.updater?.getLocalVersion();
|
|
129
|
-
if (daemonVersion) {
|
|
130
|
-
this.client
|
|
131
|
-
.postMachineHeartbeat(daemonVersion)
|
|
132
|
-
.catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
|
|
141
|
+
await this.reconcileMachineClips();
|
|
142
|
+
// Register as a hub Provider unless the machine opted out
|
|
143
|
+
// (provider_enabled=false). See applyClipProviderState.
|
|
144
|
+
await this.applyClipProviderState();
|
|
145
|
+
this.startClipReconcileTimer();
|
|
133
146
|
}
|
|
147
|
+
// Report daemon version + self-update capability via heartbeat (best-effort).
|
|
148
|
+
// Capability tracks whether a service manager supervises us: the updater is
|
|
149
|
+
// created only in that case (see isSelfUpdateManaged in index.ts). A bare
|
|
150
|
+
// `npx` foreground run reports false so the server/UI won't offer a manual
|
|
151
|
+
// update it would silently ignore.
|
|
152
|
+
this.client
|
|
153
|
+
.postMachineHeartbeat({
|
|
154
|
+
daemonVersion: this.updater?.getLocalVersion(),
|
|
155
|
+
selfUpdateCapable: this.updater != null,
|
|
156
|
+
})
|
|
157
|
+
.catch((err) => this.log.warn(`daemon heartbeat report failed: ${String(err)}`));
|
|
134
158
|
await this.fullReconcile();
|
|
135
159
|
this.ws = new ParallWs({
|
|
136
160
|
getTicket: () => this.client.getMachineWsTicket(),
|
|
@@ -185,6 +209,12 @@ export class DaemonSupervisor {
|
|
|
185
209
|
this.machineLlmSource = newSource;
|
|
186
210
|
void this.respawnAllChildren();
|
|
187
211
|
}
|
|
212
|
+
if (data.provider_enabled !== undefined &&
|
|
213
|
+
data.provider_enabled !== this.machineProviderEnabled) {
|
|
214
|
+
this.log.info(`WS: provider_enabled changed ${this.machineProviderEnabled} → ${data.provider_enabled}`);
|
|
215
|
+
this.machineProviderEnabled = data.provider_enabled;
|
|
216
|
+
void this.applyClipProviderState();
|
|
217
|
+
}
|
|
188
218
|
});
|
|
189
219
|
this.ws.on('machine.workspace.setup.requested', (data) => {
|
|
190
220
|
this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
|
|
@@ -198,9 +228,13 @@ export class DaemonSupervisor {
|
|
|
198
228
|
this.log.info(`WS: machine.stop received (reason=${data.reason ?? 'none'})`);
|
|
199
229
|
void this.stop();
|
|
200
230
|
});
|
|
201
|
-
this.ws.on('machine.clip.
|
|
202
|
-
this.log.info(`WS: clip
|
|
203
|
-
void this.
|
|
231
|
+
this.ws.on('machine.clip.sync', (data) => {
|
|
232
|
+
this.log.info(`WS: clip sync requested — clip=${data.clip_id} action=${data.action ?? ''}`);
|
|
233
|
+
void this.handleClipSync(data);
|
|
234
|
+
});
|
|
235
|
+
this.ws.on('machine.browser_profile.lifecycle', (data) => {
|
|
236
|
+
this.log.info(`WS: browser profile lifecycle requested — profile=${data.profile_id} action=${data.action}`);
|
|
237
|
+
void this.enqueueBrowserProfileLifecycle(data);
|
|
204
238
|
});
|
|
205
239
|
this.ws.onStateChange((state) => {
|
|
206
240
|
if (state === 'disconnected' || state === 'reconnecting') {
|
|
@@ -227,6 +261,7 @@ export class DaemonSupervisor {
|
|
|
227
261
|
clearTimeout(timer);
|
|
228
262
|
}
|
|
229
263
|
this.workspaceSetupRetryTimers.clear();
|
|
264
|
+
this.stopClipReconcileTimer();
|
|
230
265
|
for (const state of this.children.values()) {
|
|
231
266
|
state.shuttingDown = true;
|
|
232
267
|
if (state.restartTimer) {
|
|
@@ -243,6 +278,10 @@ export class DaemonSupervisor {
|
|
|
243
278
|
exits.push(this.clipManager.stopAll());
|
|
244
279
|
this.clipManager = null;
|
|
245
280
|
}
|
|
281
|
+
if (this.browserProfileManager) {
|
|
282
|
+
exits.push(this.browserProfileManager.stop());
|
|
283
|
+
this.browserProfileManager = null;
|
|
284
|
+
}
|
|
246
285
|
await Promise.allSettled(exits);
|
|
247
286
|
this.children.clear();
|
|
248
287
|
this.log.info('daemon supervisor stopped');
|
|
@@ -260,6 +299,8 @@ export class DaemonSupervisor {
|
|
|
260
299
|
this.machineId = machine.id;
|
|
261
300
|
this.machineOrgId = machine.org_id;
|
|
262
301
|
this.machineLlmSource = machine.llm_source ?? 'parall';
|
|
302
|
+
this.machineProviderEnabled = machine.provider_enabled ?? true;
|
|
303
|
+
this.machineClipProviderUrl = machine.clip_provider_url ?? null;
|
|
263
304
|
this.log.info(`daemon online — machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
|
|
264
305
|
return true;
|
|
265
306
|
}
|
|
@@ -268,7 +309,7 @@ export class DaemonSupervisor {
|
|
|
268
309
|
this.log.error(`getMachineSelf failed: ${String(err)} (fail-fast mode)`);
|
|
269
310
|
throw err;
|
|
270
311
|
}
|
|
271
|
-
const delay = Math.min(this.config.bootstrapBackoffMs *
|
|
312
|
+
const delay = Math.min(this.config.bootstrapBackoffMs * 2 ** attempt, this.config.bootstrapBackoffMaxMs);
|
|
272
313
|
attempt += 1;
|
|
273
314
|
this.log.warn(`getMachineSelf failed (attempt ${attempt}): ${String(err)} — retrying in ${delay}ms`);
|
|
274
315
|
const slept = await sleepCancellable(delay, signal);
|
|
@@ -331,9 +372,41 @@ export class DaemonSupervisor {
|
|
|
331
372
|
this.children.delete(userId);
|
|
332
373
|
}
|
|
333
374
|
}
|
|
334
|
-
// Reconcile
|
|
335
|
-
//
|
|
336
|
-
await this.
|
|
375
|
+
// Reconcile executable Clip definitions alongside agents (best-effort;
|
|
376
|
+
// local package install remains lazy at invoke time).
|
|
377
|
+
await this.reconcileMachineClips();
|
|
378
|
+
await this.reconcileBrowserProfiles();
|
|
379
|
+
}
|
|
380
|
+
async reconcileBrowserProfiles() {
|
|
381
|
+
if (!this.browserProfileManager)
|
|
382
|
+
return;
|
|
383
|
+
let profiles;
|
|
384
|
+
try {
|
|
385
|
+
profiles = await this.client.listMachineBrowserProfiles();
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
this.log.warn(`browser profile reconcile: list failed: ${String(err)}`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (const profile of profiles) {
|
|
392
|
+
if (profile.status !== 'running' && profile.status !== 'pending')
|
|
393
|
+
continue;
|
|
394
|
+
try {
|
|
395
|
+
if (profile.status === 'pending') {
|
|
396
|
+
await this.enqueueBrowserProfileLifecycle({
|
|
397
|
+
machine_id: profile.machine_id,
|
|
398
|
+
profile_id: profile.id,
|
|
399
|
+
action: 'open',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
await this.browserProfileManager.ensureRuntime(profile.id);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
this.log.warn(`browser profile reconcile failed for ${profile.id}: ${String(err)}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
337
410
|
}
|
|
338
411
|
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
339
412
|
/**
|
|
@@ -461,96 +534,256 @@ export class DaemonSupervisor {
|
|
|
461
534
|
}
|
|
462
535
|
}
|
|
463
536
|
}
|
|
464
|
-
async
|
|
465
|
-
if (!this.
|
|
466
|
-
this.log.warn('
|
|
467
|
-
|
|
537
|
+
async handleBrowserProfileLifecycle(data) {
|
|
538
|
+
if (!this.browserProfileManager) {
|
|
539
|
+
this.log.warn('browser profile lifecycle event received but clip runtime is disabled');
|
|
540
|
+
const status = data.action === 'stop' ? 'stopped' : 'error';
|
|
541
|
+
const error = data.action === 'stop' ? undefined : 'Browser profile runtime is disabled on this daemon';
|
|
542
|
+
await this.client.reportBrowserProfileStatus(data.profile_id, status, error).catch((err) => {
|
|
543
|
+
this.log.warn(`browser profile lifecycle negative ACK failed for ${data.profile_id}: ${String(err)}`);
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
468
546
|
}
|
|
469
547
|
try {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
548
|
+
switch (data.action) {
|
|
549
|
+
case 'open':
|
|
550
|
+
await this.browserProfileManager.openProfile(data.profile_id, data.start_url);
|
|
551
|
+
return;
|
|
552
|
+
case 'stop':
|
|
553
|
+
await this.browserProfileManager.stopProfile(data.profile_id);
|
|
554
|
+
return;
|
|
555
|
+
case 'reset':
|
|
556
|
+
await this.browserProfileManager.resetProfile(data.profile_id);
|
|
557
|
+
return;
|
|
558
|
+
default:
|
|
559
|
+
this.log.warn(`unknown browser profile lifecycle action: ${data.action}`);
|
|
481
560
|
}
|
|
482
|
-
const clipsDir = path.join(this.config.rootStateDir, 'clips');
|
|
483
|
-
// The clip record stores a resolved version; pin the install to it when
|
|
484
|
-
// the source ref is unversioned, otherwise an unversioned source like
|
|
485
|
-
// "@pinix/github-tools" would silently fetch latest instead.
|
|
486
|
-
let source = data.source_ref;
|
|
487
|
-
if (data.version) {
|
|
488
|
-
let sourceHasVersion = false;
|
|
489
|
-
try {
|
|
490
|
-
sourceHasVersion = !!parseSource(source).version;
|
|
491
|
-
}
|
|
492
|
-
catch {
|
|
493
|
-
/* fall through to append */
|
|
494
|
-
}
|
|
495
|
-
if (!sourceHasVersion)
|
|
496
|
-
source = `${source}@${data.version}`;
|
|
497
|
-
}
|
|
498
|
-
const result = await installClip({
|
|
499
|
-
source,
|
|
500
|
-
alias: data.alias,
|
|
501
|
-
clipsDir,
|
|
502
|
-
// Pinix registry REST base; falls back to the installer default when unset.
|
|
503
|
-
registryUrl: process.env.PRLL_PINIX_REGISTRY_URL?.trim() || undefined,
|
|
504
|
-
});
|
|
505
|
-
this.log.info(`clip installed: ${result.alias} v${result.version} at ${result.path}`);
|
|
506
|
-
this.clipManager.registerClip({
|
|
507
|
-
name: result.alias,
|
|
508
|
-
package: result.name,
|
|
509
|
-
version: result.version,
|
|
510
|
-
source: result.path,
|
|
511
|
-
path: result.path,
|
|
512
|
-
});
|
|
513
|
-
await this.clipManager.startClip(result.alias);
|
|
514
|
-
this.log.info(`clip started: ${result.alias}`);
|
|
515
|
-
// Report success so the server flips the desired-state row pending →
|
|
516
|
-
// installed. Reporting here (not in the caller) covers both the live WS
|
|
517
|
-
// broadcast and the durable reconcile paths with one report.
|
|
518
|
-
await this.client.reportClipInstall(data.clip_id, data.version).catch((err) => {
|
|
519
|
-
this.log.warn(`clip install report failed for ${data.alias}: ${String(err)}`);
|
|
520
|
-
});
|
|
521
|
-
return true;
|
|
522
561
|
}
|
|
523
562
|
catch (err) {
|
|
524
|
-
this.log.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
563
|
+
this.log.warn(`browser profile lifecycle failed (profile=${data.profile_id}, action=${data.action}): ${String(err)}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
enqueueBrowserProfileLifecycle(data) {
|
|
567
|
+
const previous = this.browserProfileLifecycleQueues.get(data.profile_id) ?? Promise.resolve();
|
|
568
|
+
const next = previous
|
|
569
|
+
.catch(() => {
|
|
570
|
+
// Keep the queue moving even if a prior operation failed unexpectedly.
|
|
571
|
+
})
|
|
572
|
+
.then(() => this.handleBrowserProfileLifecycle(data));
|
|
573
|
+
this.browserProfileLifecycleQueues.set(data.profile_id, next);
|
|
574
|
+
void next.finally(() => {
|
|
575
|
+
if (this.browserProfileLifecycleQueues.get(data.profile_id) === next) {
|
|
576
|
+
this.browserProfileLifecycleQueues.delete(data.profile_id);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return next;
|
|
580
|
+
}
|
|
581
|
+
async handleClipSync(_data) {
|
|
582
|
+
await this.reconcileMachineClips();
|
|
583
|
+
}
|
|
584
|
+
async reconcileMachineClips() {
|
|
585
|
+
if (this.clipReconcileInFlight) {
|
|
586
|
+
return this.clipReconcileInFlight;
|
|
587
|
+
}
|
|
588
|
+
const run = this.reconcileMachineClipsNow();
|
|
589
|
+
this.clipReconcileInFlight = run;
|
|
590
|
+
try {
|
|
591
|
+
await run;
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
if (this.clipReconcileInFlight === run) {
|
|
595
|
+
this.clipReconcileInFlight = null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async reconcileMachineClipsNow() {
|
|
600
|
+
if (!this.running) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const clipManager = this.clipManager;
|
|
604
|
+
if (!clipManager)
|
|
605
|
+
return;
|
|
536
606
|
let desired;
|
|
537
607
|
try {
|
|
538
|
-
desired = await this.client.
|
|
608
|
+
desired = await this.client.listMachineClips();
|
|
539
609
|
}
|
|
540
610
|
catch (err) {
|
|
541
|
-
this.log.warn(`
|
|
611
|
+
this.log.warn(`reconcileMachineClips: list failed: ${String(err)}`);
|
|
542
612
|
return;
|
|
543
613
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
614
|
+
if (!this.running)
|
|
615
|
+
return;
|
|
616
|
+
const desiredByAlias = new Map(desired.map((clip) => [clip.alias, clip]));
|
|
617
|
+
const registeredByName = new Map(clipManager.getRegisteredClips().map((c) => [c.name, c]));
|
|
618
|
+
let changed = false;
|
|
619
|
+
for (const registered of clipManager.getRegisteredClips()) {
|
|
620
|
+
if (desiredByAlias.has(registered.name))
|
|
621
|
+
continue;
|
|
622
|
+
await clipManager.unregisterClip(registered.name);
|
|
623
|
+
registeredByName.delete(registered.name);
|
|
624
|
+
changed = true;
|
|
625
|
+
}
|
|
626
|
+
for (const clip of desired) {
|
|
627
|
+
const config = this.machineClipToConfig(clip);
|
|
628
|
+
const existing = registeredByName.get(config.name);
|
|
629
|
+
if (!existing) {
|
|
630
|
+
clipManager.registerClip(config);
|
|
631
|
+
registeredByName.set(config.name, config);
|
|
632
|
+
changed = true;
|
|
633
|
+
}
|
|
634
|
+
else if (this.clipConfigSignature(existing) !== this.clipConfigSignature(config)) {
|
|
635
|
+
await clipManager.replaceClipConfig(config);
|
|
636
|
+
registeredByName.set(config.name, config);
|
|
637
|
+
changed = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (changed) {
|
|
641
|
+
this.log.info(`machine clips synced — count=${desired.length}`);
|
|
642
|
+
await this.reconnectClipProvider();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
startClipReconcileTimer() {
|
|
646
|
+
if (this.clipReconcileTimer || !this.clipManager)
|
|
647
|
+
return;
|
|
648
|
+
this.clipReconcileTimer = setInterval(() => {
|
|
649
|
+
void this.reconcileMachineClips().catch((err) => {
|
|
650
|
+
this.log.warn(`periodic clip reconcile failed: ${String(err)}`);
|
|
552
651
|
});
|
|
652
|
+
}, CLIP_RECONCILE_INTERVAL_MS);
|
|
653
|
+
this.clipReconcileTimer.unref?.();
|
|
654
|
+
}
|
|
655
|
+
stopClipReconcileTimer() {
|
|
656
|
+
if (!this.clipReconcileTimer)
|
|
657
|
+
return;
|
|
658
|
+
clearInterval(this.clipReconcileTimer);
|
|
659
|
+
this.clipReconcileTimer = null;
|
|
660
|
+
}
|
|
661
|
+
machineClipToConfig(clip) {
|
|
662
|
+
const clipPath = path.join(this.config.rootStateDir, 'clips', clip.alias);
|
|
663
|
+
return {
|
|
664
|
+
clipId: clip.clip_id,
|
|
665
|
+
name: clip.alias,
|
|
666
|
+
package: clip.name,
|
|
667
|
+
version: clip.version,
|
|
668
|
+
source: clip.source_ref ?? clip.name,
|
|
669
|
+
sourceType: clip.source_type,
|
|
670
|
+
sourceRef: clip.source_ref,
|
|
671
|
+
path: clipPath,
|
|
672
|
+
manifest: this.manifestFromMachineClip(clip),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
manifestFromMachineClip(clip) {
|
|
676
|
+
const raw = clip.manifest ?? {};
|
|
677
|
+
const commands = parseIpcCommands(raw.commands ?? raw.commandDetails ?? raw.command_details ?? raw.tools);
|
|
678
|
+
return {
|
|
679
|
+
name: clip.alias,
|
|
680
|
+
package: String(raw.package ?? raw.name ?? clip.name),
|
|
681
|
+
version: String(raw.version ?? clip.version ?? ''),
|
|
682
|
+
domain: typeof raw.domain === 'string' ? raw.domain : undefined,
|
|
683
|
+
description: typeof raw.description === 'string' ? raw.description : (clip.description ?? undefined),
|
|
684
|
+
commands: commands.map((c) => c.name),
|
|
685
|
+
commandDetails: commands,
|
|
686
|
+
hasWeb: Boolean(raw.hasWeb ?? raw.has_web ?? false),
|
|
687
|
+
dependencies: this.dependencyMap(raw.dependencies),
|
|
688
|
+
dependencySlots: this.dependencyMap(raw.dependencySlots ?? raw.dependency_slots),
|
|
689
|
+
patterns: Array.isArray(raw.patterns) ? raw.patterns.map(String) : undefined,
|
|
690
|
+
entities: raw.entities && typeof raw.entities === 'object'
|
|
691
|
+
? raw.entities
|
|
692
|
+
: undefined,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
dependencyMap(value) {
|
|
696
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
697
|
+
return undefined;
|
|
698
|
+
const out = {};
|
|
699
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
700
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
701
|
+
continue;
|
|
702
|
+
const item = raw;
|
|
703
|
+
out[key] = {
|
|
704
|
+
package: typeof item.package === 'string' ? item.package : undefined,
|
|
705
|
+
version: typeof item.version === 'string' ? item.version : undefined,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
clipConfigSignature(config) {
|
|
711
|
+
return JSON.stringify({
|
|
712
|
+
clipId: config.clipId,
|
|
713
|
+
name: config.name,
|
|
714
|
+
package: config.package,
|
|
715
|
+
version: config.version,
|
|
716
|
+
sourceType: config.sourceType,
|
|
717
|
+
sourceRef: config.sourceRef,
|
|
718
|
+
path: config.path,
|
|
719
|
+
manifest: config.manifest,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async ensureClipInstalled(config) {
|
|
723
|
+
if (config.sourceType !== 'registry') {
|
|
724
|
+
return config;
|
|
725
|
+
}
|
|
726
|
+
const sourceRef = config.sourceRef?.trim();
|
|
727
|
+
if (!sourceRef) {
|
|
728
|
+
throw new Error(`registry clip "${config.name}" is missing source_ref`);
|
|
729
|
+
}
|
|
730
|
+
const expectedPath = path.join(this.config.rootStateDir, 'clips', config.name);
|
|
731
|
+
const localVersion = this.readInstalledClipVersion(expectedPath);
|
|
732
|
+
if (localVersion && (!config.version || localVersion === config.version)) {
|
|
733
|
+
return { ...config, path: expectedPath, source: expectedPath };
|
|
734
|
+
}
|
|
735
|
+
let source = sourceRef;
|
|
736
|
+
if (config.version) {
|
|
737
|
+
let sourceHasVersion = false;
|
|
738
|
+
try {
|
|
739
|
+
sourceHasVersion = !!parseSource(source).version;
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
/* fall through to append */
|
|
743
|
+
}
|
|
744
|
+
if (!sourceHasVersion)
|
|
745
|
+
source = `${source}@${config.version}`;
|
|
553
746
|
}
|
|
747
|
+
const result = await installClip({
|
|
748
|
+
source,
|
|
749
|
+
alias: config.name,
|
|
750
|
+
clipsDir: path.join(this.config.rootStateDir, 'clips'),
|
|
751
|
+
registryUrl: process.env.PRLL_PINIX_REGISTRY_URL?.trim() || undefined,
|
|
752
|
+
});
|
|
753
|
+
this.log.info(`clip ensured: ${result.alias} v${result.version} at ${result.path}`);
|
|
754
|
+
return {
|
|
755
|
+
...config,
|
|
756
|
+
package: result.name,
|
|
757
|
+
version: result.version,
|
|
758
|
+
source: result.path,
|
|
759
|
+
path: result.path,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
readInstalledClipVersion(dir) {
|
|
763
|
+
for (const file of ['clip.json', 'package.json']) {
|
|
764
|
+
try {
|
|
765
|
+
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
766
|
+
const parsed = JSON.parse(raw);
|
|
767
|
+
if (typeof parsed.version === 'string' && parsed.version.trim()) {
|
|
768
|
+
return parsed.version.trim();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// try the next metadata file
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
async reconnectClipProvider() {
|
|
778
|
+
if (!this.clipProvider)
|
|
779
|
+
return;
|
|
780
|
+
const provider = this.clipProvider;
|
|
781
|
+
this.clipProvider = null;
|
|
782
|
+
this.connectedClipServiceUrl = null;
|
|
783
|
+
await provider
|
|
784
|
+
.disconnect()
|
|
785
|
+
.catch((err) => this.log.warn(`clip provider reconnect disconnect failed: ${String(err)}`));
|
|
786
|
+
await this.applyClipProviderState();
|
|
554
787
|
}
|
|
555
788
|
async handleWorkspaceSetupRequested(agentId) {
|
|
556
789
|
if (this.spawningAgents.has(agentId)) {
|
|
@@ -617,11 +850,109 @@ export class DaemonSupervisor {
|
|
|
617
850
|
this.machineLlmSource = newSource;
|
|
618
851
|
await this.respawnAllChildren();
|
|
619
852
|
}
|
|
853
|
+
const newProviderEnabled = machine.provider_enabled ?? true;
|
|
854
|
+
const newClipProviderUrl = machine.clip_provider_url ?? null;
|
|
855
|
+
const providerEnabledChanged = newProviderEnabled !== this.machineProviderEnabled;
|
|
856
|
+
const clipProviderUrlChanged = newClipProviderUrl !== this.machineClipProviderUrl;
|
|
857
|
+
if (providerEnabledChanged) {
|
|
858
|
+
this.log.info(`machine config refreshed: provider_enabled ${this.machineProviderEnabled} → ${newProviderEnabled}`);
|
|
859
|
+
this.machineProviderEnabled = newProviderEnabled;
|
|
860
|
+
}
|
|
861
|
+
if (clipProviderUrlChanged) {
|
|
862
|
+
this.log.info(`machine config refreshed: clip_provider_url ${this.machineClipProviderUrl ?? '(none)'} → ${newClipProviderUrl ?? '(none)'}`);
|
|
863
|
+
this.machineClipProviderUrl = newClipProviderUrl;
|
|
864
|
+
}
|
|
865
|
+
if (providerEnabledChanged || clipProviderUrlChanged) {
|
|
866
|
+
await this.applyClipProviderState();
|
|
867
|
+
}
|
|
620
868
|
}
|
|
621
869
|
catch (err) {
|
|
622
870
|
this.log.warn(`refreshMachineConfig failed: ${String(err)}`);
|
|
623
871
|
}
|
|
624
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Resolve the ProviderStream endpoint the daemon registers clips against.
|
|
875
|
+
* Priority: PRLL_CLIP_SERVICE_URL (hosted/cluster DNS or explicit operator
|
|
876
|
+
* override) > machine.clip_provider_url (server-delivered grey-cloud
|
|
877
|
+
* clip-rpc host for BYOC) > config.apiUrl (legacy fallback for servers
|
|
878
|
+
* predating clip_provider_url). The grey-cloud host bypasses Cloudflare's
|
|
879
|
+
* proxy, which does not pass Connect-RPC bidi streams (#1430).
|
|
880
|
+
*/
|
|
881
|
+
resolveClipServiceUrl() {
|
|
882
|
+
return (process.env.PRLL_CLIP_SERVICE_URL?.trim() ||
|
|
883
|
+
this.machineClipProviderUrl?.trim() ||
|
|
884
|
+
this.config.apiUrl);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Execution-side hub client used to resolve a Clip's dependency
|
|
888
|
+
* binding (GetBindings) and forward the dependency invoke (Invoke). It targets
|
|
889
|
+
* the same grey-cloud clip-rpc endpoint as the provider stream and auths with
|
|
890
|
+
* the daemon's mck_ (the hub scope-gates calls to instances this machine
|
|
891
|
+
* executes). Rebuilt transparently when the resolved endpoint changes.
|
|
892
|
+
*/
|
|
893
|
+
getHubClient() {
|
|
894
|
+
const serviceUrl = this.resolveClipServiceUrl();
|
|
895
|
+
if (!serviceUrl)
|
|
896
|
+
return null;
|
|
897
|
+
if (this.hubClient && this.hubClientUrl === serviceUrl)
|
|
898
|
+
return this.hubClient;
|
|
899
|
+
this.hubClient = new HubClient({ serviceUrl, authKey: this.config.apiKey });
|
|
900
|
+
this.hubClientUrl = serviceUrl;
|
|
901
|
+
return this.hubClient;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Connect, reconnect, or disconnect the clip Provider to match
|
|
905
|
+
* machineProviderEnabled and the resolved ProviderStream endpoint. An org
|
|
906
|
+
* admin toggles provider_enabled to withdraw the machine from the org clip
|
|
907
|
+
* provider pool without uninstalling clips; a changed clip_provider_url
|
|
908
|
+
* triggers a transparent reconnect to the new endpoint. No-op when the clip
|
|
909
|
+
* runtime is disabled or identity is not yet resolved. See
|
|
910
|
+
* docs/engineering-design/clip-provider-sharing-design.md.
|
|
911
|
+
*/
|
|
912
|
+
async applyClipProviderState() {
|
|
913
|
+
if (!this.clipManager || !this.machineOrgId || !this.machineId)
|
|
914
|
+
return;
|
|
915
|
+
if (this.machineProviderEnabled) {
|
|
916
|
+
const serviceUrl = this.resolveClipServiceUrl();
|
|
917
|
+
if (this.clipProvider) {
|
|
918
|
+
if (this.connectedClipServiceUrl === serviceUrl)
|
|
919
|
+
return; // already on the right endpoint
|
|
920
|
+
// Endpoint changed → tear down the stale stream before reconnecting.
|
|
921
|
+
this.log.info(`clip provider endpoint changed → reconnecting (${serviceUrl})`);
|
|
922
|
+
const stale = this.clipProvider;
|
|
923
|
+
this.clipProvider = null;
|
|
924
|
+
await stale
|
|
925
|
+
.disconnect()
|
|
926
|
+
.catch((err) => this.log.warn(`clip provider disconnect (endpoint change) failed: ${String(err)}`));
|
|
927
|
+
}
|
|
928
|
+
this.connectedClipServiceUrl = serviceUrl;
|
|
929
|
+
this.clipProvider = new ClipProvider({
|
|
930
|
+
serviceUrl,
|
|
931
|
+
authKey: this.config.apiKey,
|
|
932
|
+
orgId: this.machineOrgId,
|
|
933
|
+
providerName: `daemon-${this.machineId}`,
|
|
934
|
+
clipManager: this.clipManager,
|
|
935
|
+
log: this.log,
|
|
936
|
+
// Self-heal: if the provider can't reach its endpoint for a while, the
|
|
937
|
+
// server may have rolled out a (new) clip_provider_url after we booted
|
|
938
|
+
// (our ws-gateway WS never bounced to re-trigger machine.hello). Refetch
|
|
939
|
+
// machine config; applyClipProviderState rebuilds us if the URL moved.
|
|
940
|
+
onPersistentFailure: () => void this.refreshMachineConfig(),
|
|
941
|
+
});
|
|
942
|
+
this.clipProvider
|
|
943
|
+
.connect()
|
|
944
|
+
.catch((err) => this.log.warn(`clip provider connect failed: ${String(err)}`));
|
|
945
|
+
}
|
|
946
|
+
else if (this.clipProvider) {
|
|
947
|
+
this.log.info('clip provider disabled (provider_enabled=false) — disconnecting');
|
|
948
|
+
const provider = this.clipProvider;
|
|
949
|
+
this.clipProvider = null;
|
|
950
|
+
this.connectedClipServiceUrl = null;
|
|
951
|
+
await provider
|
|
952
|
+
.disconnect()
|
|
953
|
+
.catch((err) => this.log.warn(`clip provider disconnect failed: ${String(err)}`));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
625
956
|
async respawnAllChildren() {
|
|
626
957
|
const states = [...this.children.values()];
|
|
627
958
|
for (const state of states) {
|
|
@@ -744,9 +1075,10 @@ export class DaemonSupervisor {
|
|
|
744
1075
|
workspaceDir: state.workspacePath,
|
|
745
1076
|
claudeHome: state.claudeHome,
|
|
746
1077
|
};
|
|
747
|
-
|
|
1078
|
+
let baseEnv = { ...process.env, PRLL_API_URL: this.config.apiUrl };
|
|
748
1079
|
if (this.machineId)
|
|
749
1080
|
baseEnv.PRLL_MACHINE_ID = this.machineId;
|
|
1081
|
+
baseEnv = applyRuntimeBinaryEnv(state.runtimeType, baseEnv, this.log);
|
|
750
1082
|
// An explicit per-agent provider_config (llm_source or BYO creds) wins;
|
|
751
1083
|
// otherwise defer to the CURRENT machine-level llm_source. The server sends
|
|
752
1084
|
// an empty `{}` for self-hosted agents (per-agent provider_config is
|
|
@@ -783,7 +1115,7 @@ export class DaemonSupervisor {
|
|
|
783
1115
|
}
|
|
784
1116
|
if (wasShutting || !this.running)
|
|
785
1117
|
return;
|
|
786
|
-
const delay = Math.min(this.config.restartBackoffMs *
|
|
1118
|
+
const delay = Math.min(this.config.restartBackoffMs * 2 ** state.restartAttempts, this.config.restartBackoffMaxMs);
|
|
787
1119
|
state.restartAttempts += 1;
|
|
788
1120
|
this.log.warn(`agent ${state.agentId} will restart in ${delay}ms`);
|
|
789
1121
|
state.restartTimer = setTimeout(() => {
|