@parall/daemon 1.30.0 → 1.32.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.
Files changed (53) hide show
  1. package/bundle/manifest.json +17 -11
  2. package/bundle/package.json +1 -0
  3. package/bundle/parall-claude-agent.js +27671 -337
  4. package/bundle/parall-codex-agent.js +27715 -375
  5. package/bundle/parall-daemon.js +30421 -1355
  6. package/bundle/parall-openclaw-agent.js +51 -26
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +122 -72
  10. package/dist/clip-runtime/clip-installer.d.ts +44 -0
  11. package/dist/clip-runtime/clip-installer.d.ts.map +1 -0
  12. package/dist/clip-runtime/clip-installer.js +501 -0
  13. package/dist/clip-runtime/clip-provider.d.ts +76 -0
  14. package/dist/clip-runtime/clip-provider.d.ts.map +1 -0
  15. package/dist/clip-runtime/clip-provider.js +402 -0
  16. package/dist/clip-runtime/index.d.ts +7 -0
  17. package/dist/clip-runtime/index.d.ts.map +1 -0
  18. package/dist/clip-runtime/index.js +5 -0
  19. package/dist/clip-runtime/ipc.d.ts +94 -0
  20. package/dist/clip-runtime/ipc.d.ts.map +1 -0
  21. package/dist/clip-runtime/ipc.js +98 -0
  22. package/dist/clip-runtime/manifest.d.ts +74 -0
  23. package/dist/clip-runtime/manifest.d.ts.map +1 -0
  24. package/dist/clip-runtime/manifest.js +181 -0
  25. package/dist/clip-runtime/process-manager.d.ts +57 -0
  26. package/dist/clip-runtime/process-manager.d.ts.map +1 -0
  27. package/dist/clip-runtime/process-manager.js +354 -0
  28. package/dist/clip-runtime/process.d.ts +59 -0
  29. package/dist/clip-runtime/process.d.ts.map +1 -0
  30. package/dist/clip-runtime/process.js +350 -0
  31. package/dist/config.d.ts +13 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +36 -19
  34. package/dist/filesystem.d.ts +1 -1
  35. package/dist/filesystem.d.ts.map +1 -1
  36. package/dist/filesystem.js +51 -53
  37. package/dist/index.js +46 -14
  38. package/dist/runtimes.d.ts +10 -8
  39. package/dist/runtimes.d.ts.map +1 -1
  40. package/dist/runtimes.js +63 -95
  41. package/dist/supervisor.d.ts +12 -3
  42. package/dist/supervisor.d.ts.map +1 -1
  43. package/dist/supervisor.js +272 -71
  44. package/dist/updater-manifest.d.ts +41 -0
  45. package/dist/updater-manifest.d.ts.map +1 -0
  46. package/dist/updater-manifest.js +94 -0
  47. package/dist/updater.d.ts +60 -0
  48. package/dist/updater.d.ts.map +1 -0
  49. package/dist/updater.js +427 -0
  50. package/dist/workspace.d.ts +2 -2
  51. package/dist/workspace.d.ts.map +1 -1
  52. package/dist/workspace.js +112 -112
  53. package/package.json +6 -6
@@ -1,15 +1,18 @@
1
- import { spawn } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { ParallWs } from "@parall/sdk";
5
- import { listDirectory } from "./filesystem.js";
6
- import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from "./config.js";
7
- import { assertAgentKey, getRuntimeAdapter } from "./runtimes.js";
8
- import { prepareWorkspace } from "./workspace.js";
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { effectiveLLMSourceExplicit } from '@parall/agent-core';
5
+ import { ParallWs, } from '@parall/sdk';
6
+ import { listDirectory } from './filesystem.js';
7
+ import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from './config.js';
8
+ import { assertAgentKey, getRuntimeAdapter } from './runtimes.js';
9
+ import { prepareWorkspace } from './workspace.js';
10
+ import { ClipProcessManager, ClipProvider } from './clip-runtime/index.js';
11
+ import { installClip, parseSource } from './clip-runtime/clip-installer.js';
9
12
  const RUNTIME_PACKAGES = {
10
13
  'claude-code': '@parall/claude-agent',
11
- 'codex': '@parall/codex-agent',
12
- 'openclaw': '@parall/openclaw-agent',
14
+ codex: '@parall/codex-agent',
15
+ openclaw: '@parall/openclaw-agent',
13
16
  };
14
17
  const WORKSPACE_SETUP_RETRY_DELAY_MS = 5_000;
15
18
  /**
@@ -22,14 +25,14 @@ function sleepCancellable(ms, signal) {
22
25
  return Promise.resolve(false);
23
26
  return new Promise((resolve) => {
24
27
  const timer = setTimeout(() => {
25
- signal.removeEventListener("abort", onAbort);
28
+ signal.removeEventListener('abort', onAbort);
26
29
  resolve(true);
27
30
  }, ms);
28
31
  const onAbort = () => {
29
32
  clearTimeout(timer);
30
33
  resolve(false);
31
34
  };
32
- signal.addEventListener("abort", onAbort, { once: true });
35
+ signal.addEventListener('abort', onAbort, { once: true });
33
36
  });
34
37
  }
35
38
  export { sleepCancellable };
@@ -55,79 +58,152 @@ export class DaemonSupervisor {
55
58
  cancelledSpawns = new Set();
56
59
  ws = null;
57
60
  running = false;
61
+ machineId = null;
58
62
  machineOrgId = null;
59
- machineLlmSource = "parall";
63
+ machineLlmSource = 'parall';
60
64
  stopResolve = null;
65
+ updater = null;
66
+ healthConfirmed = false;
67
+ clipManager = null;
68
+ clipProvider = null;
61
69
  constructor(config, client, log) {
62
70
  this.config = config;
63
71
  this.client = client;
64
72
  this.log = log;
65
73
  }
74
+ setUpdater(updater) {
75
+ this.updater = updater;
76
+ }
66
77
  /** Start the supervisor. Returns a promise that resolves on `stop()`. */
67
78
  async run(signal) {
68
79
  if (this.running)
69
- throw new Error("supervisor already running");
80
+ throw new Error('supervisor already running');
70
81
  this.running = true;
71
82
  const onAbort = () => {
72
83
  this.stop().catch((err) => this.log.error(`stop() failed: ${String(err)}`));
73
84
  };
74
- signal.addEventListener("abort", onAbort, { once: true });
85
+ signal.addEventListener('abort', onAbort, { once: true });
75
86
  try {
76
87
  if (!(await this.bootstrapWithRetry(signal))) {
77
- signal.removeEventListener("abort", onAbort);
88
+ signal.removeEventListener('abort', onAbort);
78
89
  this.running = false;
79
90
  return;
80
91
  }
81
92
  }
82
93
  catch (err) {
83
- signal.removeEventListener("abort", onAbort);
94
+ signal.removeEventListener('abort', onAbort);
84
95
  this.running = false;
85
96
  throw err;
86
97
  }
87
98
  this.migrateFlatLayout();
99
+ // Initialize Clip runtime manager (optional, for Pinix Clip subprocess management)
100
+ if (process.env.PRLL_CLIP_RUNTIME_ENABLED === 'true') {
101
+ this.clipManager = new ClipProcessManager({
102
+ clipsDir: path.join(this.config.rootStateDir, 'clips'),
103
+ dataDir: path.join(this.config.rootStateDir, 'clip-data'),
104
+ });
105
+ try {
106
+ await this.clipManager.loadInstalledClips();
107
+ }
108
+ catch (err) {
109
+ this.log.warn(`clip runtime init failed: ${String(err)}`);
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)}`));
133
+ }
88
134
  await this.fullReconcile();
89
135
  this.ws = new ParallWs({
90
136
  getTicket: () => this.client.getMachineWsTicket(),
91
137
  wsUrl: this.config.wsUrl,
92
138
  reconnect: true,
93
139
  });
94
- this.ws.on("machine.hello", (_data) => {
95
- this.log.info("machine WS connected (machine.hello)");
140
+ this.ws.on('machine.hello', (_data) => {
141
+ this.log.info('machine WS connected (machine.hello)');
142
+ if (!this.healthConfirmed && this.updater) {
143
+ try {
144
+ this.updater.confirmVersion();
145
+ this.healthConfirmed = true;
146
+ }
147
+ catch (err) {
148
+ this.log.warn(`confirmVersion failed: ${String(err)}`);
149
+ }
150
+ }
96
151
  void (async () => {
97
152
  await this.refreshMachineConfig();
98
153
  await this.fullReconcile();
99
154
  })();
100
155
  });
101
- this.ws.on("machine.agent.attached", (data) => {
156
+ this.ws.on('machine.update', (data) => {
157
+ this.log.info(`WS: daemon update available — version=${data.new_version} mandatory=${data.mandatory}`);
158
+ if (this.updater) {
159
+ void this.updater
160
+ .triggerUpdate(data.new_version, data.mandatory)
161
+ .then(async (applied) => {
162
+ if (applied) {
163
+ this.log.info('daemon update applied — stopping supervisor before restart');
164
+ await this.stop();
165
+ process.exit(42);
166
+ }
167
+ })
168
+ .catch((err) => {
169
+ this.log.warn(`daemon update failed: ${String(err)}`);
170
+ });
171
+ }
172
+ });
173
+ this.ws.on('machine.agent.attached', (data) => {
102
174
  this.log.info(`WS: agent ${data.agent_id} attached`);
103
175
  void this.handleAgentAttached(data.agent_id);
104
176
  });
105
- this.ws.on("machine.agent.detached", (data) => {
177
+ this.ws.on('machine.agent.detached', (data) => {
106
178
  this.log.info(`WS: agent ${data.agent_id} detached`);
107
179
  void this.handleAgentDetached(data.agent_id);
108
180
  });
109
- this.ws.on("machine.config.updated", (data) => {
110
- const newSource = data.llm_source ?? "parall";
181
+ this.ws.on('machine.config.updated', (data) => {
182
+ const newSource = data.llm_source ?? 'parall';
111
183
  if (newSource !== this.machineLlmSource) {
112
184
  this.log.info(`WS: llm_source changed ${this.machineLlmSource} → ${newSource}, respawning all agents`);
113
185
  this.machineLlmSource = newSource;
114
186
  void this.respawnAllChildren();
115
187
  }
116
188
  });
117
- this.ws.on("machine.workspace.setup.requested", (data) => {
189
+ this.ws.on('machine.workspace.setup.requested', (data) => {
118
190
  this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
119
191
  void this.handleWorkspaceSetupRequested(data.agent_id);
120
192
  });
121
- this.ws.on("machine.filesystem.browse", (data) => {
193
+ this.ws.on('machine.filesystem.browse', (data) => {
122
194
  this.log.info(`WS: filesystem browse requested: ${data.path}`);
123
195
  void this.handleFilesystemBrowse(data.request_id, data.path);
124
196
  });
125
- this.ws.on("machine.stop", (data) => {
126
- this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
197
+ this.ws.on('machine.stop', (data) => {
198
+ this.log.info(`WS: machine.stop received (reason=${data.reason ?? 'none'})`);
127
199
  void this.stop();
128
200
  });
201
+ this.ws.on('machine.clip.install', (data) => {
202
+ this.log.info(`WS: clip install requested — alias=${data.alias} source=${data.source_ref}`);
203
+ void this.handleClipInstall(data);
204
+ });
129
205
  this.ws.onStateChange((state) => {
130
- if (state === "disconnected" || state === "reconnecting") {
206
+ if (state === 'disconnected' || state === 'reconnecting') {
131
207
  this.log.warn(`machine WS state: ${state}`);
132
208
  }
133
209
  });
@@ -135,7 +211,7 @@ export class DaemonSupervisor {
135
211
  await new Promise((resolve) => {
136
212
  this.stopResolve = resolve;
137
213
  });
138
- signal.removeEventListener("abort", onAbort);
214
+ signal.removeEventListener('abort', onAbort);
139
215
  }
140
216
  /** Disconnect WS, cancel timers, SIGTERM all children, await exit. */
141
217
  async stop() {
@@ -159,9 +235,17 @@ export class DaemonSupervisor {
159
235
  }
160
236
  exits.push(this.terminateChild(state));
161
237
  }
238
+ if (this.clipProvider) {
239
+ exits.push(this.clipProvider.disconnect());
240
+ this.clipProvider = null;
241
+ }
242
+ if (this.clipManager) {
243
+ exits.push(this.clipManager.stopAll());
244
+ this.clipManager = null;
245
+ }
162
246
  await Promise.allSettled(exits);
163
247
  this.children.clear();
164
- this.log.info("daemon supervisor stopped");
248
+ this.log.info('daemon supervisor stopped');
165
249
  if (this.stopResolve) {
166
250
  this.stopResolve();
167
251
  this.stopResolve = null;
@@ -173,8 +257,9 @@ export class DaemonSupervisor {
173
257
  while (this.running && !signal.aborted) {
174
258
  try {
175
259
  const machine = await this.client.getMachineSelf();
260
+ this.machineId = machine.id;
176
261
  this.machineOrgId = machine.org_id;
177
- this.machineLlmSource = machine.llm_source ?? "parall";
262
+ this.machineLlmSource = machine.llm_source ?? 'parall';
178
263
  this.log.info(`daemon online — machine_id=${machine.id} org=${machine.org_id} label=${machine.label}`);
179
264
  return true;
180
265
  }
@@ -216,7 +301,7 @@ export class DaemonSupervisor {
216
301
  this.log.warn(`skipping attached entry with no user_id (profile=${JSON.stringify(a.profile)})`);
217
302
  continue;
218
303
  }
219
- if (a.user && a.user.status !== "active") {
304
+ if (a.user && a.user.status !== 'active') {
220
305
  this.log.info(`agent ${userId} not active (status=${a.user.status}) — skipping`);
221
306
  continue;
222
307
  }
@@ -231,7 +316,7 @@ export class DaemonSupervisor {
231
316
  await this.spawnAgent(userId, orgId, a);
232
317
  }
233
318
  else if (!existing.child && !existing.restartTimer && !existing.shuttingDown) {
234
- await this.restartChildNow(existing, "reconcile found no live child");
319
+ await this.restartChildNow(existing, 'reconcile found no live child');
235
320
  }
236
321
  }
237
322
  for (const [userId, state] of this.children) {
@@ -246,6 +331,9 @@ export class DaemonSupervisor {
246
331
  this.children.delete(userId);
247
332
  }
248
333
  }
334
+ // Reconcile desired clip installs alongside agents (best-effort; never
335
+ // blocks agent reconcile).
336
+ await this.reconcileClipInstalls();
249
337
  }
250
338
  // ---- Flat layout migration (self-hosted → daemon) ----
251
339
  /**
@@ -255,21 +343,21 @@ export class DaemonSupervisor {
255
343
  */
256
344
  migrateFlatLayout() {
257
345
  const root = this.config.rootStateDir;
258
- const agentsDir = path.join(root, "agents");
259
- const flatWorkspace = path.join(root, "workspace");
346
+ const agentsDir = path.join(root, 'agents');
347
+ const flatWorkspace = path.join(root, 'workspace');
260
348
  if (!fs.existsSync(flatWorkspace) || fs.existsSync(agentsDir))
261
349
  return;
262
350
  let ownerAgentId;
263
- const sessionsDir = path.join(root, "sessions");
351
+ const sessionsDir = path.join(root, 'sessions');
264
352
  if (fs.existsSync(sessionsDir)) {
265
353
  try {
266
354
  for (const file of fs.readdirSync(sessionsDir)) {
267
- if (!file.endsWith(".json"))
355
+ if (!file.endsWith('.json'))
268
356
  continue;
269
- const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
357
+ const decoded = Buffer.from(file.replace('.json', ''), 'base64url').toString();
270
358
  // runtimeKey format: "agent:main:{runtime}:{agentId}:orchestrator"
271
- const parts = decoded.split(":");
272
- if (parts.length >= 4 && parts[3].startsWith("usr_")) {
359
+ const parts = decoded.split(':');
360
+ if (parts.length >= 4 && parts[3].startsWith('usr_')) {
273
361
  ownerAgentId = parts[3];
274
362
  break;
275
363
  }
@@ -279,11 +367,11 @@ export class DaemonSupervisor {
279
367
  // best-effort scan
280
368
  }
281
369
  }
282
- const targetId = ownerAgentId ?? "_orphan";
370
+ const targetId = ownerAgentId ?? '_orphan';
283
371
  const targetDir = path.join(agentsDir, targetId);
284
372
  try {
285
373
  fs.mkdirSync(targetDir, { recursive: true });
286
- for (const sub of ["workspace", "sessions", "dispatch-context"]) {
374
+ for (const sub of ['workspace', 'sessions', 'dispatch-context']) {
287
375
  const src = path.join(root, sub);
288
376
  if (fs.existsSync(src)) {
289
377
  fs.renameSync(src, path.join(targetDir, sub));
@@ -303,24 +391,24 @@ export class DaemonSupervisor {
303
391
  }
304
392
  catch (err) {
305
393
  this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
306
- return { kind: "retryable" };
394
+ return { kind: 'retryable' };
307
395
  }
308
396
  const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
309
397
  if (!entry) {
310
398
  this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
311
- return { kind: "skip" };
399
+ return { kind: 'skip' };
312
400
  }
313
- if (entry.user && entry.user.status !== "active") {
401
+ if (entry.user && entry.user.status !== 'active') {
314
402
  this.log.info(`agent ${agentId} not active (status=${entry.user.status}) — skipping`);
315
- return { kind: "skip" };
403
+ return { kind: 'skip' };
316
404
  }
317
- return { kind: "found", entry };
405
+ return { kind: 'found', entry };
318
406
  }
319
407
  async handleAgentAttached(agentId) {
320
408
  if (this.children.has(agentId) || this.spawningAgents.has(agentId))
321
409
  return;
322
410
  const result = await this.fetchAttachedAgent(agentId);
323
- if (result.kind !== "found") {
411
+ if (result.kind !== 'found') {
324
412
  return;
325
413
  }
326
414
  const orgId = this.machineOrgId;
@@ -373,6 +461,97 @@ export class DaemonSupervisor {
373
461
  }
374
462
  }
375
463
  }
464
+ async handleClipInstall(data) {
465
+ if (!this.clipManager) {
466
+ this.log.warn('clip install event received but clip runtime is disabled');
467
+ return false;
468
+ }
469
+ try {
470
+ // Idempotency guard: if the clip is already installed and running
471
+ // locally, a prior install succeeded but its server ACK
472
+ // (reportClipInstall) may have failed — the desired-state row is still
473
+ // pending, so reconcile re-sent the same directive. Don't re-download or
474
+ // restart a healthy clip; just retry the report so pending → installed.
475
+ if (this.clipManager.isRunning(data.alias)) {
476
+ this.log.info(`clip already running, re-reporting install: ${data.alias}`);
477
+ await this.client.reportClipInstall(data.clip_id, data.version).catch((err) => {
478
+ this.log.warn(`clip install re-report failed for ${data.alias}: ${String(err)}`);
479
+ });
480
+ return true;
481
+ }
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
+ }
523
+ catch (err) {
524
+ this.log.error(`clip install failed: ${String(err)}`);
525
+ return false;
526
+ }
527
+ }
528
+ // Durable reconcile: pull the registry clips this machine should have (covers
529
+ // installs broadcast while the daemon was offline, and clips targeted at
530
+ // machines that joined later), install the missing ones, and report success
531
+ // so the server flips the desired-state row pending → installed. Mirrors the
532
+ // listAttachedAgents reconcile pattern; runs on every fullReconcile.
533
+ async reconcileClipInstalls() {
534
+ if (!this.clipManager)
535
+ return; // clip runtime disabled — nothing to reconcile
536
+ let desired;
537
+ try {
538
+ desired = await this.client.listClipInstalls();
539
+ }
540
+ catch (err) {
541
+ this.log.warn(`reconcileClipInstalls: list failed: ${String(err)}`);
542
+ return;
543
+ }
544
+ for (const d of desired) {
545
+ // handleClipInstall reports success to the server on its own, so the
546
+ // reconcile loop just drives the install for each desired clip.
547
+ await this.handleClipInstall({
548
+ clip_id: d.clip_id,
549
+ alias: d.alias,
550
+ source_ref: d.source_ref ?? '',
551
+ version: d.version,
552
+ });
553
+ }
554
+ }
376
555
  async handleWorkspaceSetupRequested(agentId) {
377
556
  if (this.spawningAgents.has(agentId)) {
378
557
  this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
@@ -380,12 +559,12 @@ export class DaemonSupervisor {
380
559
  return;
381
560
  }
382
561
  const result = await this.fetchAttachedAgent(agentId);
383
- if (result.kind === "retryable") {
562
+ if (result.kind === 'retryable') {
384
563
  this.pendingWorkspaceSetup.add(agentId);
385
564
  this.scheduleWorkspaceSetupRetry(agentId);
386
565
  return;
387
566
  }
388
- if (result.kind === "skip") {
567
+ if (result.kind === 'skip') {
389
568
  this.pendingWorkspaceSetup.delete(agentId);
390
569
  this.clearWorkspaceSetupRetry(agentId);
391
570
  return;
@@ -432,7 +611,7 @@ export class DaemonSupervisor {
432
611
  async refreshMachineConfig() {
433
612
  try {
434
613
  const machine = await this.client.getMachineSelf();
435
- const newSource = machine.llm_source ?? "parall";
614
+ const newSource = machine.llm_source ?? 'parall';
436
615
  if (newSource !== this.machineLlmSource) {
437
616
  this.log.info(`machine config refreshed: llm_source ${this.machineLlmSource} → ${newSource}`);
438
617
  this.machineLlmSource = newSource;
@@ -450,7 +629,7 @@ export class DaemonSupervisor {
450
629
  }
451
630
  for (const state of states) {
452
631
  if (!state.shuttingDown && this.running) {
453
- await this.restartChildNow(state, "llm_source changed");
632
+ await this.restartChildNow(state, 'llm_source changed');
454
633
  }
455
634
  }
456
635
  }
@@ -513,7 +692,7 @@ export class DaemonSupervisor {
513
692
  catch (err) {
514
693
  this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
515
694
  }
516
- const runtimeType = attached.profile.runtime_type ?? "claude-code";
695
+ const runtimeType = attached.profile.runtime_type ?? 'claude-code';
517
696
  let workspaceDir;
518
697
  try {
519
698
  workspaceDir = await prepareWorkspace({
@@ -538,7 +717,10 @@ export class DaemonSupervisor {
538
717
  runtimeType,
539
718
  workspacePath: workspaceDir,
540
719
  claudeHome,
541
- providerConfig: attached.provider_config ?? { llm_source: this.machineLlmSource },
720
+ // Store the RAW per-agent provider_config; the machine-level fallback is
721
+ // resolved at startChild time (see resolveProviderConfig) so a respawn
722
+ // after a machine llm_source change picks up the new source.
723
+ providerConfig: attached.provider_config,
542
724
  child: null,
543
725
  credential,
544
726
  restartAttempts: 0,
@@ -562,11 +744,25 @@ export class DaemonSupervisor {
562
744
  workspaceDir: state.workspacePath,
563
745
  claudeHome: state.claudeHome,
564
746
  };
565
- const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
566
- this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
567
- const child = spawn(adapter.bin, [], {
747
+ const baseEnv = { ...process.env, PRLL_API_URL: this.config.apiUrl };
748
+ if (this.machineId)
749
+ baseEnv.PRLL_MACHINE_ID = this.machineId;
750
+ // An explicit per-agent provider_config (llm_source or BYO creds) wins;
751
+ // otherwise defer to the CURRENT machine-level llm_source. The server sends
752
+ // an empty `{}` for self-hosted agents (per-agent provider_config is
753
+ // rejected there), and `{} ?? machineLlmSource` would keep the `{}` and
754
+ // resolve to "parall", silently shadowing the machine's choice — so check
755
+ // for an explicit signal. Resolved here (not at spawn) so a respawn after a
756
+ // machine llm_source change applies the new source without a full restart.
757
+ const effectiveProviderConfig = effectiveLLMSourceExplicit(state.providerConfig)
758
+ ? state.providerConfig
759
+ : { llm_source: this.machineLlmSource };
760
+ const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
761
+ const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(' ')}` : adapter.bin;
762
+ this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
763
+ const child = spawn(adapter.bin, adapter.args, {
568
764
  env,
569
- stdio: ["ignore", "inherit", "inherit"],
765
+ stdio: ['ignore', 'inherit', 'inherit'],
570
766
  detached: false,
571
767
  });
572
768
  state.child = child;
@@ -580,10 +776,10 @@ export class DaemonSupervisor {
580
776
  const wasShutting = state.shuttingDown;
581
777
  state.child = null;
582
778
  if (err) {
583
- this.log.error(`agent ${state.agentId} child ${event}: ${String(err)}${wasShutting ? " (shutting down)" : ""}`);
779
+ this.log.error(`agent ${state.agentId} child ${event}: ${String(err)}${wasShutting ? ' (shutting down)' : ''}`);
584
780
  }
585
781
  else {
586
- this.log.info(`agent ${state.agentId} exited code=${code ?? "null"} signal=${signal ?? "null"}${wasShutting ? " (shutting down)" : ""}`);
782
+ this.log.info(`agent ${state.agentId} exited code=${code ?? 'null'} signal=${signal ?? 'null'}${wasShutting ? ' (shutting down)' : ''}`);
587
783
  }
588
784
  if (wasShutting || !this.running)
589
785
  return;
@@ -595,14 +791,19 @@ export class DaemonSupervisor {
595
791
  this.startChild(state);
596
792
  }, delay);
597
793
  };
598
- child.once("error", (err) => {
794
+ child.once('error', (err) => {
599
795
  if (err.code === 'ENOENT') {
600
- const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
601
- this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
796
+ if (adapter.args.length > 0) {
797
+ this.log.error(`Node runtime "${adapter.bin}" not found is the Desktop app installed?`);
798
+ }
799
+ else {
800
+ const pkg = RUNTIME_PACKAGES[state.runtimeType] ?? `@parall/${state.runtimeType}-agent`;
801
+ this.log.error(`Runtime binary "${adapter.bin}" not found in PATH. Install: npm install -g ${pkg}`);
802
+ }
602
803
  }
603
- settleChild("error", null, null, err);
804
+ settleChild('error', null, null, err);
604
805
  });
605
- child.once("close", (code, signal) => settleChild("close", code, signal));
806
+ child.once('close', (code, signal) => settleChild('close', code, signal));
606
807
  const stableTimer = setTimeout(() => {
607
808
  if (state.child === child) {
608
809
  state.restartAttempts = 0;
@@ -616,25 +817,25 @@ export class DaemonSupervisor {
616
817
  return;
617
818
  return new Promise((resolve) => {
618
819
  const onExit = () => resolve();
619
- child.once("exit", onExit);
820
+ child.once('exit', onExit);
620
821
  try {
621
- child.kill("SIGTERM");
822
+ child.kill('SIGTERM');
622
823
  }
623
824
  catch (err) {
624
825
  this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
625
- child.off("exit", onExit);
826
+ child.off('exit', onExit);
626
827
  resolve();
627
828
  return;
628
829
  }
629
830
  const hardKill = setTimeout(() => {
630
831
  try {
631
- child.kill("SIGKILL");
832
+ child.kill('SIGKILL');
632
833
  }
633
834
  catch {
634
835
  /* already gone */
635
836
  }
636
837
  }, 10_000);
637
- child.once("exit", () => clearTimeout(hardKill));
838
+ child.once('exit', () => clearTimeout(hardKill));
638
839
  });
639
840
  }
640
841
  ensureSharedCredentialLink(agentClaudeHome, agentId) {
@@ -661,7 +862,7 @@ export class DaemonSupervisor {
661
862
  }
662
863
  }
663
864
  catch (err) {
664
- if (err.code !== "ENOENT") {
865
+ if (err.code !== 'ENOENT') {
665
866
  throw err;
666
867
  }
667
868
  }
@@ -0,0 +1,41 @@
1
+ export interface ManifestFile {
2
+ sha256: string;
3
+ size: number;
4
+ }
5
+ export interface RemoteManifest {
6
+ version: string;
7
+ built_at: string;
8
+ min_daemon_version?: string;
9
+ min_node_version?: string;
10
+ files: Record<string, ManifestFile>;
11
+ signature: string;
12
+ }
13
+ export interface LocalManifest {
14
+ version: string;
15
+ built_at: string;
16
+ min_node_version?: string;
17
+ files: Record<string, ManifestFile>;
18
+ }
19
+ /**
20
+ * Recursively sort all object keys at every depth for deterministic JSON output.
21
+ * Arrays preserve order; primitives pass through.
22
+ */
23
+ export declare function canonicalize(obj: unknown): unknown;
24
+ /**
25
+ * Verify the Ed25519 signature of a remote manifest.
26
+ * Signs canonical JSON of all fields except `signature`.
27
+ */
28
+ export declare function verifyManifestSignature(manifest: RemoteManifest, publicKey: string): boolean;
29
+ /**
30
+ * Compare two semver strings (including prerelease tags). Returns:
31
+ * -1 if a < b
32
+ * 0 if a == b
33
+ * 1 if a > b
34
+ * null if either is not valid semver
35
+ *
36
+ * Prerelease ordering follows SemVer 2.0: a version with prerelease has
37
+ * lower precedence than the same version without prerelease. Prerelease
38
+ * identifiers are compared lexicographically when both present.
39
+ */
40
+ export declare function semverCompare(a: string, b: string): number | null;
41
+ //# sourceMappingURL=updater-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updater-manifest.d.ts","sourceRoot":"","sources":["../src/updater-manifest.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUlD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAQ5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBjE"}