@parall/daemon 1.29.3 → 1.31.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.
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { ParallWs } from "@parall/sdk";
5
+ import { listDirectory } from "./filesystem.js";
5
6
  import { agentClaudeCredentialsFileFor, agentClaudeHomeFor, agentStateDirFor, agentWorkspaceDirFor, sharedClaudeCredentialsFileFor, } from "./config.js";
6
7
  import { assertAgentKey, getRuntimeAdapter } from "./runtimes.js";
7
8
  import { prepareWorkspace } from "./workspace.js";
@@ -57,11 +58,16 @@ export class DaemonSupervisor {
57
58
  machineOrgId = null;
58
59
  machineLlmSource = "parall";
59
60
  stopResolve = null;
61
+ updater = null;
62
+ healthConfirmed = false;
60
63
  constructor(config, client, log) {
61
64
  this.config = config;
62
65
  this.client = client;
63
66
  this.log = log;
64
67
  }
68
+ setUpdater(updater) {
69
+ this.updater = updater;
70
+ }
65
71
  /** Start the supervisor. Returns a promise that resolves on `stop()`. */
66
72
  async run(signal) {
67
73
  if (this.running)
@@ -84,6 +90,11 @@ export class DaemonSupervisor {
84
90
  throw err;
85
91
  }
86
92
  this.migrateFlatLayout();
93
+ // Report daemon version via heartbeat (best-effort)
94
+ const daemonVersion = this.updater?.getLocalVersion();
95
+ if (daemonVersion) {
96
+ this.client.postMachineHeartbeat(daemonVersion).catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
97
+ }
87
98
  await this.fullReconcile();
88
99
  this.ws = new ParallWs({
89
100
  getTicket: () => this.client.getMachineWsTicket(),
@@ -92,11 +103,34 @@ export class DaemonSupervisor {
92
103
  });
93
104
  this.ws.on("machine.hello", (_data) => {
94
105
  this.log.info("machine WS connected (machine.hello)");
106
+ if (!this.healthConfirmed && this.updater) {
107
+ try {
108
+ this.updater.confirmVersion();
109
+ this.healthConfirmed = true;
110
+ }
111
+ catch (err) {
112
+ this.log.warn(`confirmVersion failed: ${String(err)}`);
113
+ }
114
+ }
95
115
  void (async () => {
96
116
  await this.refreshMachineConfig();
97
117
  await this.fullReconcile();
98
118
  })();
99
119
  });
120
+ this.ws.on("machine.update", (data) => {
121
+ this.log.info(`WS: daemon update available — version=${data.new_version} mandatory=${data.mandatory}`);
122
+ if (this.updater) {
123
+ void this.updater.triggerUpdate(data.new_version, data.mandatory).then(async (applied) => {
124
+ if (applied) {
125
+ this.log.info("daemon update applied — stopping supervisor before restart");
126
+ await this.stop();
127
+ process.exit(42);
128
+ }
129
+ }).catch((err) => {
130
+ this.log.warn(`daemon update failed: ${String(err)}`);
131
+ });
132
+ }
133
+ });
100
134
  this.ws.on("machine.agent.attached", (data) => {
101
135
  this.log.info(`WS: agent ${data.agent_id} attached`);
102
136
  void this.handleAgentAttached(data.agent_id);
@@ -117,6 +151,10 @@ export class DaemonSupervisor {
117
151
  this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
118
152
  void this.handleWorkspaceSetupRequested(data.agent_id);
119
153
  });
154
+ this.ws.on("machine.filesystem.browse", (data) => {
155
+ this.log.info(`WS: filesystem browse requested: ${data.path}`);
156
+ void this.handleFilesystemBrowse(data.request_id, data.path);
157
+ });
120
158
  this.ws.on("machine.stop", (data) => {
121
159
  this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
122
160
  void this.stop();
@@ -343,6 +381,31 @@ export class DaemonSupervisor {
343
381
  await this.terminateChild(state);
344
382
  this.children.delete(agentId);
345
383
  }
384
+ async handleFilesystemBrowse(requestId, dirPath) {
385
+ try {
386
+ const result = await listDirectory(dirPath);
387
+ await this.client.postBrowseResponse(requestId, {
388
+ request_id: requestId,
389
+ path: dirPath,
390
+ entries: result.entries,
391
+ error: result.error,
392
+ });
393
+ }
394
+ catch (err) {
395
+ this.log.warn(`filesystem browse failed: ${String(err)}`);
396
+ try {
397
+ await this.client.postBrowseResponse(requestId, {
398
+ request_id: requestId,
399
+ path: dirPath,
400
+ entries: [],
401
+ error: String(err),
402
+ });
403
+ }
404
+ catch {
405
+ // best-effort
406
+ }
407
+ }
408
+ }
346
409
  async handleWorkspaceSetupRequested(agentId) {
347
410
  if (this.spawningAgents.has(agentId)) {
348
411
  this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
@@ -0,0 +1,39 @@
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
+ files: Record<string, ManifestFile>;
10
+ signature: string;
11
+ }
12
+ export interface LocalManifest {
13
+ version: string;
14
+ built_at: string;
15
+ files: Record<string, ManifestFile>;
16
+ }
17
+ /**
18
+ * Recursively sort all object keys at every depth for deterministic JSON output.
19
+ * Arrays preserve order; primitives pass through.
20
+ */
21
+ export declare function canonicalize(obj: unknown): unknown;
22
+ /**
23
+ * Verify the Ed25519 signature of a remote manifest.
24
+ * Signs canonical JSON of all fields except `signature`.
25
+ */
26
+ export declare function verifyManifestSignature(manifest: RemoteManifest, publicKey: string): boolean;
27
+ /**
28
+ * Compare two semver strings (including prerelease tags). Returns:
29
+ * -1 if a < b
30
+ * 0 if a == b
31
+ * 1 if a > b
32
+ * null if either is not valid semver
33
+ *
34
+ * Prerelease ordering follows SemVer 2.0: a version with prerelease has
35
+ * lower precedence than the same version without prerelease. Prerelease
36
+ * identifiers are compared lexicographically when both present.
37
+ */
38
+ export declare function semverCompare(a: string, b: string): number | null;
39
+ //# 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,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,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"}
@@ -0,0 +1,94 @@
1
+ import { verify } from "node:crypto";
2
+ /**
3
+ * Recursively sort all object keys at every depth for deterministic JSON output.
4
+ * Arrays preserve order; primitives pass through.
5
+ */
6
+ export function canonicalize(obj) {
7
+ if (Array.isArray(obj))
8
+ return obj.map(canonicalize);
9
+ if (obj !== null && typeof obj === "object") {
10
+ const sorted = {};
11
+ for (const k of Object.keys(obj).sort()) {
12
+ sorted[k] = canonicalize(obj[k]);
13
+ }
14
+ return sorted;
15
+ }
16
+ return obj;
17
+ }
18
+ /**
19
+ * Verify the Ed25519 signature of a remote manifest.
20
+ * Signs canonical JSON of all fields except `signature`.
21
+ */
22
+ export function verifyManifestSignature(manifest, publicKey) {
23
+ const { signature, ...rest } = manifest;
24
+ const canonical = JSON.stringify(canonicalize(rest));
25
+ try {
26
+ return verify(null, Buffer.from(canonical), publicKey, Buffer.from(signature, "base64"));
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Compare two semver strings (including prerelease tags). Returns:
34
+ * -1 if a < b
35
+ * 0 if a == b
36
+ * 1 if a > b
37
+ * null if either is not valid semver
38
+ *
39
+ * Prerelease ordering follows SemVer 2.0: a version with prerelease has
40
+ * lower precedence than the same version without prerelease. Prerelease
41
+ * identifiers are compared lexicographically when both present.
42
+ */
43
+ export function semverCompare(a, b) {
44
+ const pa = parseSemver(a);
45
+ const pb = parseSemver(b);
46
+ if (!pa || !pb)
47
+ return null;
48
+ for (let i = 0; i < 3; i++) {
49
+ if (pa.nums[i] < pb.nums[i])
50
+ return -1;
51
+ if (pa.nums[i] > pb.nums[i])
52
+ return 1;
53
+ }
54
+ // Same major.minor.patch — compare prerelease:
55
+ // no prerelease > has prerelease (per SemVer §11)
56
+ if (!pa.pre && !pb.pre)
57
+ return 0;
58
+ if (!pa.pre)
59
+ return 1;
60
+ if (!pb.pre)
61
+ return -1;
62
+ // Both have prerelease: lexicographic comparison
63
+ if (pa.pre < pb.pre)
64
+ return -1;
65
+ if (pa.pre > pb.pre)
66
+ return 1;
67
+ return 0;
68
+ }
69
+ function parseSemver(v) {
70
+ // Split off prerelease at the first hyphen in the patch component.
71
+ // e.g. "0.0.0-staging.abc1234" → major=0, minor=0, patch="0", pre="staging.abc1234"
72
+ const parts = v.split(".");
73
+ if (parts.length < 3)
74
+ return null;
75
+ // Rejoin any dots beyond the first two (prerelease may contain dots)
76
+ const major = Number(parts[0]);
77
+ const minor = Number(parts[1]);
78
+ const rest = parts.slice(2).join(".");
79
+ // Split patch from prerelease: "0-staging.abc" → patch="0", pre="staging.abc"
80
+ const hyphen = rest.indexOf("-");
81
+ let patchStr;
82
+ let pre;
83
+ if (hyphen >= 0) {
84
+ patchStr = rest.slice(0, hyphen);
85
+ pre = rest.slice(hyphen + 1);
86
+ }
87
+ else {
88
+ patchStr = rest;
89
+ }
90
+ const patch = Number(patchStr);
91
+ if (isNaN(major) || isNaN(minor) || isNaN(patch))
92
+ return null;
93
+ return { nums: [major, minor, patch], pre };
94
+ }
@@ -0,0 +1,60 @@
1
+ import type { GatewayLogger } from "@parall/agent-core";
2
+ import { type LocalManifest } from "./updater-manifest.js";
3
+ export declare class DaemonUpdater {
4
+ private readonly bundleDir;
5
+ private readonly cdnBaseUrl;
6
+ private readonly log;
7
+ private readonly signingEnabled;
8
+ private updating;
9
+ private periodicTimer;
10
+ constructor(bundleDir: string, cdnBaseUrl: string, log: GatewayLogger, signingEnabled: boolean);
11
+ /**
12
+ * Start periodic update checks. Call once after supervisor bootstrap.
13
+ */
14
+ startPeriodicCheck(intervalMs: number): void;
15
+ stopPeriodicCheck(): void;
16
+ /**
17
+ * Check CDN for a newer version. Returns true if update was applied
18
+ * (caller should exit for service manager restart).
19
+ */
20
+ checkAndApply(targetVersion?: string): Promise<boolean>;
21
+ /**
22
+ * Handle WS-triggered update. Adds jitter for non-mandatory updates.
23
+ */
24
+ triggerUpdate(targetVersion: string, mandatory: boolean): Promise<boolean>;
25
+ /**
26
+ * Check if we need to roll back from a failed update.
27
+ * Call on every daemon startup, before bootstrap.
28
+ * Returns true if rollback was performed (caller should exit immediately).
29
+ */
30
+ checkRollback(): boolean;
31
+ /**
32
+ * Confirm the current version after successful health check.
33
+ * Clears pending state so rollback won't trigger.
34
+ */
35
+ confirmVersion(): void;
36
+ /**
37
+ * Get the current local manifest version (for heartbeat reporting).
38
+ */
39
+ getLocalVersion(): string | undefined;
40
+ /**
41
+ * Check CDN for available update without downloading.
42
+ * Returns remote version info for CLI --check display.
43
+ */
44
+ checkAvailable(): Promise<{
45
+ available: boolean;
46
+ currentVersion?: string;
47
+ remoteVersion?: string;
48
+ }>;
49
+ private doUpdate;
50
+ private atomicSwap;
51
+ private swapSymlink;
52
+ private pruneOldVersions;
53
+ loadLocalManifest(): LocalManifest | null;
54
+ private loadUpdateState;
55
+ private saveUpdateState;
56
+ private httpGet;
57
+ private downloadFile;
58
+ private cleanDir;
59
+ }
60
+ //# sourceMappingURL=updater.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../src/updater.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAEL,KAAK,aAAa,EAGnB,MAAM,uBAAuB,CAAC;AAoB/B,qBAAa,aAAa;IAKtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAPjC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAA+B;gBAGjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,aAAa,EAClB,cAAc,EAAE,OAAO;IAK1C;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAU5C,iBAAiB,IAAI,IAAI;IAOzB;;;OAGG;IACG,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa7D;;OAEG;IACG,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAQhF;;;;OAIG;IACH,aAAa,IAAI,OAAO;IA6CxB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAYtB;;OAEG;IACH,eAAe,IAAI,MAAM,GAAG,SAAS;IAIrC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;YAmB1F,QAAQ;IAqGtB,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,gBAAgB;IAmBxB,iBAAiB,IAAI,aAAa,GAAG,IAAI;IAUzC,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,YAAY;IAoCpB,OAAO,CAAC,QAAQ;CAKjB"}