@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.
- package/bundle/manifest.json +12 -11
- package/bundle/parall-claude-agent.js +252 -85
- package/bundle/parall-codex-agent.js +248 -94
- package/bundle/parall-daemon.js +1076 -299
- package/bundle/parall-openclaw-agent.js +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -2
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/filesystem.d.ts +7 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +118 -0
- package/dist/index.js +30 -3
- package/dist/runtimes.d.ts +9 -8
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +47 -83
- package/dist/supervisor.d.ts +5 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +63 -0
- package/dist/updater-manifest.d.ts +39 -0
- package/dist/updater-manifest.d.ts.map +1 -0
- package/dist/updater-manifest.js +94 -0
- package/dist/updater.d.ts +60 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +409 -0
- package/package.json +6 -6
package/dist/supervisor.js
CHANGED
|
@@ -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"}
|