@parall/daemon 1.29.0 → 1.29.1
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 +8 -8
- package/bundle/parall-claude-agent.js +129 -25
- package/bundle/parall-codex-agent.js +205 -28
- package/bundle/parall-daemon.js +723 -106
- package/dist/config.d.ts +3 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/index.js +2 -1
- package/dist/runtimes.d.ts +1 -0
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +1 -1
- package/dist/supervisor.d.ts +9 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +130 -16
- package/dist/workspace.d.ts +11 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +436 -0
- package/package.json +6 -6
package/bundle/parall-daemon.js
CHANGED
|
@@ -22,6 +22,8 @@ var ENDPOINTS = {
|
|
|
22
22
|
AUTH_CHECK_EMAIL: `${API_BASE}/auth/check-email`,
|
|
23
23
|
AUTH_VERIFY_EMAIL: `${API_BASE}/auth/verify-email`,
|
|
24
24
|
AUTH_RESEND_CODE: `${API_BASE}/auth/resend-code`,
|
|
25
|
+
AUTH_FORGOT_PASSWORD: `${API_BASE}/auth/forgot-password`,
|
|
26
|
+
AUTH_RESET_PASSWORD: `${API_BASE}/auth/reset-password`,
|
|
25
27
|
// Users
|
|
26
28
|
USERS_ME: `${API_BASE}/users/me`,
|
|
27
29
|
USER_AVATAR: `${API_BASE}/users/me/avatar`,
|
|
@@ -56,6 +58,9 @@ var ENDPOINTS = {
|
|
|
56
58
|
// Messages (global, by message ID)
|
|
57
59
|
MESSAGE: (id) => `${API_BASE}/messages/${id}`,
|
|
58
60
|
MESSAGE_REPLIES: (id) => `${API_BASE}/messages/${id}/replies`,
|
|
61
|
+
MESSAGE_WATCH: (id) => `${API_BASE}/messages/${id}/watch`,
|
|
62
|
+
MESSAGE_WATCHERS: (id) => `${API_BASE}/messages/${id}/watchers`,
|
|
63
|
+
MESSAGE_WATCHING: (id) => `${API_BASE}/messages/${id}/watching`,
|
|
59
64
|
// Upload (org-scoped)
|
|
60
65
|
UPLOAD_PRESIGN: (orgId) => `${API_BASE}/orgs/${orgId}/upload/presign`,
|
|
61
66
|
UPLOAD_COMPLETE: (orgId) => `${API_BASE}/orgs/${orgId}/upload/complete`,
|
|
@@ -106,6 +111,9 @@ var ENDPOINTS = {
|
|
|
106
111
|
// Attach/Detach bind an agent to/from a daemon Machine.
|
|
107
112
|
MACHINE_ATTACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
|
|
108
113
|
MACHINE_DETACH_AGENT: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}`,
|
|
114
|
+
MACHINE_WORKSPACE_STATES: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/workspace-states`,
|
|
115
|
+
MACHINE_AGENT_DAEMON_CONFIG: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/daemon-config`,
|
|
116
|
+
MACHINE_AGENT_WORKSPACE_SETUP: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/workspace/setup`,
|
|
109
117
|
MACHINE_LLM_SOURCE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/llm-source`,
|
|
110
118
|
MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
|
|
111
119
|
MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
|
|
@@ -119,6 +127,7 @@ var ENDPOINTS = {
|
|
|
119
127
|
MACHINES_ME_AGENTS: `${API_BASE}/machines/me/agents`,
|
|
120
128
|
MACHINES_ME_HEALTH: `${API_BASE}/machines/me/health`,
|
|
121
129
|
MACHINES_ME_AGENT_LAUNCH_CREDENTIAL: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/launch-credential`,
|
|
130
|
+
MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
|
|
122
131
|
MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
|
|
123
132
|
// Tasks (org-scoped)
|
|
124
133
|
TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
|
|
@@ -298,7 +307,8 @@ var WS_EVENTS = {
|
|
|
298
307
|
MACHINE_HELLO: "machine.hello",
|
|
299
308
|
MACHINE_AGENT_ATTACHED: "machine.agent.attached",
|
|
300
309
|
MACHINE_AGENT_DETACHED: "machine.agent.detached",
|
|
301
|
-
MACHINE_STOP: "machine.stop"
|
|
310
|
+
MACHINE_STOP: "machine.stop",
|
|
311
|
+
MACHINE_WORKSPACE_SETUP_REQUESTED: "machine.workspace.setup.requested"
|
|
302
312
|
};
|
|
303
313
|
|
|
304
314
|
// ts/sdk/dist/client.js
|
|
@@ -318,7 +328,9 @@ var ParallClient = class _ParallClient {
|
|
|
318
328
|
"/auth/logout",
|
|
319
329
|
"/auth/verify-email",
|
|
320
330
|
"/auth/resend-code",
|
|
321
|
-
"/auth/check-email"
|
|
331
|
+
"/auth/check-email",
|
|
332
|
+
"/auth/forgot-password",
|
|
333
|
+
"/auth/reset-password"
|
|
322
334
|
]);
|
|
323
335
|
/** Proactive refresh when token expires within this window (seconds). */
|
|
324
336
|
static REFRESH_THRESHOLD_S = 5 * 60;
|
|
@@ -379,10 +391,10 @@ var ParallClient = class _ParallClient {
|
|
|
379
391
|
* REFRESH_THRESHOLD_S, refresh it **before** sending the request.
|
|
380
392
|
* No-op when the token is still fresh, missing, or un-parseable.
|
|
381
393
|
*/
|
|
382
|
-
async ensureFreshToken(
|
|
394
|
+
async ensureFreshToken(path6) {
|
|
383
395
|
if (!this.token || !this.getRefreshToken)
|
|
384
396
|
return;
|
|
385
|
-
const pathSuffix =
|
|
397
|
+
const pathSuffix = path6.replace(/^\/api\/v1/, "");
|
|
386
398
|
if (_ParallClient.AUTH_PATHS.has(pathSuffix))
|
|
387
399
|
return;
|
|
388
400
|
const exp = _ParallClient.decodeJwtExp(this.token);
|
|
@@ -414,11 +426,11 @@ var ParallClient = class _ParallClient {
|
|
|
414
426
|
this.refreshPromise = null;
|
|
415
427
|
}
|
|
416
428
|
}
|
|
417
|
-
async request(method,
|
|
429
|
+
async request(method, path6, body, query, retried = false, opts) {
|
|
418
430
|
if (!retried) {
|
|
419
|
-
await this.ensureFreshToken(
|
|
431
|
+
await this.ensureFreshToken(path6);
|
|
420
432
|
}
|
|
421
|
-
let url = `${this.baseUrl}${
|
|
433
|
+
let url = `${this.baseUrl}${path6}`;
|
|
422
434
|
if (query) {
|
|
423
435
|
const params = new URLSearchParams();
|
|
424
436
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -443,12 +455,12 @@ var ParallClient = class _ParallClient {
|
|
|
443
455
|
throw _ParallClient.normalizeFetchError(err);
|
|
444
456
|
}
|
|
445
457
|
if (res.status === 401) {
|
|
446
|
-
const pathSuffix =
|
|
458
|
+
const pathSuffix = path6.replace(/^\/api\/v1/, "");
|
|
447
459
|
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
448
460
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
449
461
|
const refreshed = await this.tryRefresh();
|
|
450
462
|
if (refreshed) {
|
|
451
|
-
return this.request(method,
|
|
463
|
+
return this.request(method, path6, body, query, true, opts);
|
|
452
464
|
}
|
|
453
465
|
}
|
|
454
466
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -485,15 +497,15 @@ var ParallClient = class _ParallClient {
|
|
|
485
497
|
* hit the 100 MiB cap, so a longer 5-minute timeout is used so a
|
|
486
498
|
* 50 MiB blob on a slow connection doesn't get chopped at 15 s.
|
|
487
499
|
*/
|
|
488
|
-
async multipartRequest(method,
|
|
500
|
+
async multipartRequest(method, path6, body, retried = false) {
|
|
489
501
|
if (!retried) {
|
|
490
|
-
await this.ensureFreshToken(
|
|
502
|
+
await this.ensureFreshToken(path6);
|
|
491
503
|
}
|
|
492
504
|
const { "Content-Type": _drop, ...headers } = this.buildHeaders();
|
|
493
505
|
void _drop;
|
|
494
506
|
let res;
|
|
495
507
|
try {
|
|
496
|
-
res = await fetch(`${this.baseUrl}${
|
|
508
|
+
res = await fetch(`${this.baseUrl}${path6}`, {
|
|
497
509
|
method,
|
|
498
510
|
headers,
|
|
499
511
|
body,
|
|
@@ -503,12 +515,12 @@ var ParallClient = class _ParallClient {
|
|
|
503
515
|
throw _ParallClient.normalizeFetchError(err);
|
|
504
516
|
}
|
|
505
517
|
if (res.status === 401) {
|
|
506
|
-
const pathSuffix =
|
|
518
|
+
const pathSuffix = path6.replace(/^\/api\/v1/, "");
|
|
507
519
|
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
508
520
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
509
521
|
const refreshed = await this.tryRefresh();
|
|
510
522
|
if (refreshed) {
|
|
511
|
-
return this.multipartRequest(method,
|
|
523
|
+
return this.multipartRequest(method, path6, body, true);
|
|
512
524
|
}
|
|
513
525
|
}
|
|
514
526
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -556,6 +568,12 @@ var ParallClient = class _ParallClient {
|
|
|
556
568
|
async resendCode(email) {
|
|
557
569
|
return this.request("POST", ENDPOINTS.AUTH_RESEND_CODE, { email });
|
|
558
570
|
}
|
|
571
|
+
async forgotPassword(email) {
|
|
572
|
+
return this.request("POST", ENDPOINTS.AUTH_FORGOT_PASSWORD, { email });
|
|
573
|
+
}
|
|
574
|
+
async resetPassword(token, newPassword) {
|
|
575
|
+
return this.request("POST", ENDPOINTS.AUTH_RESET_PASSWORD, { token, new_password: newPassword });
|
|
576
|
+
}
|
|
559
577
|
// ---- WebSocket ----
|
|
560
578
|
async getWsTicket() {
|
|
561
579
|
return this.request("POST", ENDPOINTS.WS_TICKET);
|
|
@@ -762,8 +780,8 @@ var ParallClient = class _ParallClient {
|
|
|
762
780
|
async completeUpload(orgId, attachmentId) {
|
|
763
781
|
return this.request("POST", ENDPOINTS.UPLOAD_COMPLETE(orgId), { attachment_id: attachmentId });
|
|
764
782
|
}
|
|
765
|
-
async getFileUrl(id) {
|
|
766
|
-
return this.request("GET", ENDPOINTS.FILE(id));
|
|
783
|
+
async getFileUrl(id, opts) {
|
|
784
|
+
return this.request("GET", ENDPOINTS.FILE(id), void 0, opts?.download ? { download: true } : void 0);
|
|
767
785
|
}
|
|
768
786
|
// ---- Approvals ----
|
|
769
787
|
async getApproval(id) {
|
|
@@ -960,6 +978,16 @@ var ParallClient = class _ParallClient {
|
|
|
960
978
|
async detachAgent(orgId, machineId, agentId) {
|
|
961
979
|
return this.request("DELETE", ENDPOINTS.MACHINE_DETACH_AGENT(orgId, machineId, agentId));
|
|
962
980
|
}
|
|
981
|
+
async listAgentWorkspaceStates(orgId, machineId) {
|
|
982
|
+
const res = await this.request("GET", ENDPOINTS.MACHINE_WORKSPACE_STATES(orgId, machineId));
|
|
983
|
+
return res.data;
|
|
984
|
+
}
|
|
985
|
+
async patchAgentDaemonConfig(orgId, machineId, agentId, daemonConfig) {
|
|
986
|
+
return this.request("PATCH", ENDPOINTS.MACHINE_AGENT_DAEMON_CONFIG(orgId, machineId, agentId), { daemon_config: daemonConfig });
|
|
987
|
+
}
|
|
988
|
+
async retryAgentWorkspaceSetup(orgId, machineId, agentId) {
|
|
989
|
+
return this.request("POST", ENDPOINTS.MACHINE_AGENT_WORKSPACE_SETUP(orgId, machineId, agentId));
|
|
990
|
+
}
|
|
963
991
|
async patchMachineLLMSource(orgId, machineId, llmSource2) {
|
|
964
992
|
return this.request("PATCH", ENDPOINTS.MACHINE_LLM_SOURCE(orgId, machineId), { llm_source: llmSource2 });
|
|
965
993
|
}
|
|
@@ -1005,6 +1033,12 @@ var ParallClient = class _ParallClient {
|
|
|
1005
1033
|
async postMachineHeartbeat() {
|
|
1006
1034
|
return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH);
|
|
1007
1035
|
}
|
|
1036
|
+
async reportAgentWorkspaceState(agentId, state) {
|
|
1037
|
+
const res = await this.request("PUT", ENDPOINTS.MACHINES_ME_AGENT_WORKSPACE_STATE(agentId), state);
|
|
1038
|
+
if (res?.status === "ignored_stale") {
|
|
1039
|
+
throw new ApiError(409, "Workspace state report ignored as stale", "STALE_WORKSPACE_STATE");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1008
1042
|
/**
|
|
1009
1043
|
* `POST /machines/me/agents/{agentId}/launch-credential` — mint a
|
|
1010
1044
|
* short-lived `agk_*` for one of this Machine's attached agents. The
|
|
@@ -1154,6 +1188,20 @@ var ParallClient = class _ParallClient {
|
|
|
1154
1188
|
const res = await this.request("GET", ENDPOINTS.TASK_WATCHERS(orgId, taskId));
|
|
1155
1189
|
return res.data;
|
|
1156
1190
|
}
|
|
1191
|
+
async watchThread(threadRootId) {
|
|
1192
|
+
return this.request("POST", ENDPOINTS.MESSAGE_WATCH(threadRootId));
|
|
1193
|
+
}
|
|
1194
|
+
async unwatchThread(threadRootId) {
|
|
1195
|
+
return this.request("DELETE", ENDPOINTS.MESSAGE_WATCH(threadRootId));
|
|
1196
|
+
}
|
|
1197
|
+
async getThreadWatchers(threadRootId) {
|
|
1198
|
+
const res = await this.request("GET", ENDPOINTS.MESSAGE_WATCHERS(threadRootId));
|
|
1199
|
+
return res.data;
|
|
1200
|
+
}
|
|
1201
|
+
async isWatchingThread(threadRootId) {
|
|
1202
|
+
const res = await this.request("GET", ENDPOINTS.MESSAGE_WATCHING(threadRootId));
|
|
1203
|
+
return res.watching;
|
|
1204
|
+
}
|
|
1157
1205
|
async getSubtasks(orgId, taskId) {
|
|
1158
1206
|
const res = await this.request("GET", ENDPOINTS.TASK_SUBTASKS(orgId, taskId));
|
|
1159
1207
|
return res.data;
|
|
@@ -1356,8 +1404,8 @@ var ParallClient = class _ParallClient {
|
|
|
1356
1404
|
async deleteWikiPathScope(orgId, wikiId, scopeId) {
|
|
1357
1405
|
await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
|
|
1358
1406
|
}
|
|
1359
|
-
async getWikiAccessStatus(orgId, wikiId,
|
|
1360
|
-
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0,
|
|
1407
|
+
async getWikiAccessStatus(orgId, wikiId, path6) {
|
|
1408
|
+
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path6 ? { path: path6 } : void 0);
|
|
1361
1409
|
}
|
|
1362
1410
|
async createWikiAccessRequest(orgId, wikiId, data) {
|
|
1363
1411
|
await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
|
|
@@ -1366,11 +1414,11 @@ var ParallClient = class _ParallClient {
|
|
|
1366
1414
|
async getWikiCommits(orgId, wikiId, params) {
|
|
1367
1415
|
return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
|
|
1368
1416
|
}
|
|
1369
|
-
async getWikiFileCommits(orgId, wikiId,
|
|
1370
|
-
return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path:
|
|
1417
|
+
async getWikiFileCommits(orgId, wikiId, path6, params) {
|
|
1418
|
+
return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path6, ...params });
|
|
1371
1419
|
}
|
|
1372
|
-
async getWikiBlame(orgId, wikiId,
|
|
1373
|
-
return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path:
|
|
1420
|
+
async getWikiBlame(orgId, wikiId, path6, ref) {
|
|
1421
|
+
return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path6, ref });
|
|
1374
1422
|
}
|
|
1375
1423
|
// ---- Wiki Operations (audit log) ----
|
|
1376
1424
|
async getWikiOperations(orgId, wikiId, params) {
|
|
@@ -1450,14 +1498,28 @@ var ParallClient = class _ParallClient {
|
|
|
1450
1498
|
params.set("machine_id", opts.machine_id);
|
|
1451
1499
|
if (opts?.unresolved_machine)
|
|
1452
1500
|
params.set("unresolved_machine", "true");
|
|
1501
|
+
if (opts?.from)
|
|
1502
|
+
params.set("from", opts.from);
|
|
1503
|
+
if (opts?.to)
|
|
1504
|
+
params.set("to", opts.to);
|
|
1453
1505
|
const qs = params.toString();
|
|
1454
1506
|
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}${qs ? `?${qs}` : ""}`);
|
|
1455
1507
|
}
|
|
1456
|
-
async listBillingTransactionAgentGroups(orgId) {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1508
|
+
async listBillingTransactionAgentGroups(orgId, opts) {
|
|
1509
|
+
const params = new URLSearchParams({ group_by: "agent" });
|
|
1510
|
+
if (opts?.from)
|
|
1511
|
+
params.set("from", opts.from);
|
|
1512
|
+
if (opts?.to)
|
|
1513
|
+
params.set("to", opts.to);
|
|
1514
|
+
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?${params}`);
|
|
1515
|
+
}
|
|
1516
|
+
async listBillingTransactionMachineGroups(orgId, opts) {
|
|
1517
|
+
const params = new URLSearchParams({ group_by: "machine" });
|
|
1518
|
+
if (opts?.from)
|
|
1519
|
+
params.set("from", opts.from);
|
|
1520
|
+
if (opts?.to)
|
|
1521
|
+
params.set("to", opts.to);
|
|
1522
|
+
return this.request("GET", `${ENDPOINTS.BILLING_TRANSACTIONS(orgId)}?${params}`);
|
|
1461
1523
|
}
|
|
1462
1524
|
async createCheckout(orgId, req) {
|
|
1463
1525
|
return this.request("POST", ENDPOINTS.BILLING_CHECKOUT(orgId), req);
|
|
@@ -1508,6 +1570,12 @@ var ApiError = class extends Error {
|
|
|
1508
1570
|
};
|
|
1509
1571
|
|
|
1510
1572
|
// ts/sdk/dist/ws.js
|
|
1573
|
+
function isRetryableNetworkError(err) {
|
|
1574
|
+
return err instanceof Error && err.name === "ApiError" && "status" in err && err.status === 0;
|
|
1575
|
+
}
|
|
1576
|
+
function isBrowserRuntime() {
|
|
1577
|
+
return typeof window !== "undefined";
|
|
1578
|
+
}
|
|
1511
1579
|
var ParallWs = class {
|
|
1512
1580
|
ws = null;
|
|
1513
1581
|
options;
|
|
@@ -1543,7 +1611,11 @@ var ParallWs = class {
|
|
|
1543
1611
|
try {
|
|
1544
1612
|
ticket = await this.options.getTicket();
|
|
1545
1613
|
} catch (err) {
|
|
1546
|
-
|
|
1614
|
+
if (isBrowserRuntime() && isRetryableNetworkError(err)) {
|
|
1615
|
+
console.warn("Failed to get WS ticket:", err);
|
|
1616
|
+
} else {
|
|
1617
|
+
console.error("Failed to get WS ticket:", err);
|
|
1618
|
+
}
|
|
1547
1619
|
if (this.options.reconnect) {
|
|
1548
1620
|
this.scheduleReconnect();
|
|
1549
1621
|
} else {
|
|
@@ -1901,6 +1973,7 @@ function resolveClaudeDaemonConfig(env = process.env) {
|
|
|
1901
1973
|
rootStateDir,
|
|
1902
1974
|
rootClaudeHome,
|
|
1903
1975
|
wsUrl: env.PRLL_WS_URL?.trim() || void 0,
|
|
1976
|
+
swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || void 0,
|
|
1904
1977
|
pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
|
|
1905
1978
|
heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
|
|
1906
1979
|
restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
|
|
@@ -1932,14 +2005,19 @@ function agentClaudeCredentialsFileFor(agentClaudeHome) {
|
|
|
1932
2005
|
function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
1933
2006
|
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
|
|
1934
2007
|
}
|
|
1935
|
-
function resolveWsUrl(apiUrl, explicitWsUrl) {
|
|
1936
|
-
|
|
2008
|
+
function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
|
|
2009
|
+
const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
2010
|
+
if (!swimlaneName)
|
|
2011
|
+
return base;
|
|
2012
|
+
const url = new URL(base);
|
|
2013
|
+
url.searchParams.set("swimlane", swimlaneName);
|
|
2014
|
+
return url.toString();
|
|
1937
2015
|
}
|
|
1938
2016
|
|
|
1939
2017
|
// ts/daemon/dist/supervisor.js
|
|
1940
|
-
import { spawn } from "node:child_process";
|
|
1941
|
-
import * as
|
|
1942
|
-
import * as
|
|
2018
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2019
|
+
import * as fs3 from "node:fs";
|
|
2020
|
+
import * as path4 from "node:path";
|
|
1943
2021
|
|
|
1944
2022
|
// ts/daemon/dist/runtimes.js
|
|
1945
2023
|
import * as path2 from "node:path";
|
|
@@ -2058,23 +2136,448 @@ function assertAgentKey(apiKey) {
|
|
|
2058
2136
|
}
|
|
2059
2137
|
}
|
|
2060
2138
|
|
|
2139
|
+
// ts/daemon/dist/workspace.js
|
|
2140
|
+
import { spawn } from "node:child_process";
|
|
2141
|
+
import { createHash } from "node:crypto";
|
|
2142
|
+
import * as fs2 from "node:fs";
|
|
2143
|
+
import * as path3 from "node:path";
|
|
2144
|
+
var OUTPUT_TAIL_LIMIT = 32 * 1024;
|
|
2145
|
+
var DEFAULT_SETUP_TIMEOUT_SEC = 600;
|
|
2146
|
+
async function prepareWorkspace(opts) {
|
|
2147
|
+
const prior = opts.attached.workspace_state;
|
|
2148
|
+
const plan = buildWorkspacePlan(opts.attached.daemon_config, opts.defaultWorkspaceDir, prior?.config_hash);
|
|
2149
|
+
let forceSetup = false;
|
|
2150
|
+
if (prior?.status === "ready" && prior.config_hash === plan.configHash) {
|
|
2151
|
+
if (await verifyExistingWorkspace(plan, opts.log)) {
|
|
2152
|
+
return plan.workspaceDir;
|
|
2153
|
+
}
|
|
2154
|
+
forceSetup = true;
|
|
2155
|
+
}
|
|
2156
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2157
|
+
config_hash: plan.configHash,
|
|
2158
|
+
status: "preparing"
|
|
2159
|
+
});
|
|
2160
|
+
let outputTail = "";
|
|
2161
|
+
try {
|
|
2162
|
+
await ensureWorkspace(plan, opts.log);
|
|
2163
|
+
if (shouldRunSetup(plan.workspace.setup, prior, plan.configHash, forceSetup)) {
|
|
2164
|
+
outputTail = await runSetup(plan.workspaceDir, plan.workspace.setup.command, plan.workspace.setup?.timeout_sec);
|
|
2165
|
+
}
|
|
2166
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2167
|
+
config_hash: plan.configHash,
|
|
2168
|
+
status: "ready",
|
|
2169
|
+
output_tail: outputTail || null
|
|
2170
|
+
});
|
|
2171
|
+
return plan.workspaceDir;
|
|
2172
|
+
} catch (err) {
|
|
2173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2174
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2175
|
+
config_hash: plan.configHash,
|
|
2176
|
+
status: "failed",
|
|
2177
|
+
last_error: message,
|
|
2178
|
+
output_tail: outputTail || null
|
|
2179
|
+
}).catch((reportErr) => {
|
|
2180
|
+
opts.log.warn(`workspace state report failed after setup error: ${String(reportErr)}`);
|
|
2181
|
+
});
|
|
2182
|
+
throw err;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function buildWorkspacePlan(input, defaultWorkspaceDir, serverConfigHash) {
|
|
2186
|
+
const config = normalizeConfig(input, defaultWorkspaceDir);
|
|
2187
|
+
const workspace = config.workspace;
|
|
2188
|
+
const { workspaceDir, customWorkspaceField } = resolveWorkspaceDir(workspace, defaultWorkspaceDir);
|
|
2189
|
+
const configHash = serverConfigHash || createHash("sha256").update(JSON.stringify(config)).digest("hex");
|
|
2190
|
+
return { config, workspace, workspaceDir, defaultWorkspaceDir, customWorkspaceField, configHash };
|
|
2191
|
+
}
|
|
2192
|
+
function normalizeConfig(input, defaultWorkspaceDir) {
|
|
2193
|
+
const cfg = input ? JSON.parse(JSON.stringify(input)) : {};
|
|
2194
|
+
cfg.workspace_path = cfg.workspace_path?.trim() || void 0;
|
|
2195
|
+
if (!cfg.workspace && cfg.workspace_path) {
|
|
2196
|
+
cfg.workspace = { mode: "local_path", path: cfg.workspace_path };
|
|
2197
|
+
}
|
|
2198
|
+
if (!cfg.workspace) {
|
|
2199
|
+
cfg.workspace = { mode: "default" };
|
|
2200
|
+
}
|
|
2201
|
+
const ws = cfg.workspace;
|
|
2202
|
+
ws.mode = ws.mode?.trim() || void 0;
|
|
2203
|
+
ws.path = ws.path?.trim() || void 0;
|
|
2204
|
+
if (ws.git) {
|
|
2205
|
+
ws.git.remote = ws.git.remote?.trim() || void 0;
|
|
2206
|
+
ws.git.ref = ws.git.ref?.trim() || void 0;
|
|
2207
|
+
ws.git.target_path = ws.git.target_path?.trim() || void 0;
|
|
2208
|
+
}
|
|
2209
|
+
ws.mode ||= ws.git ? "git" : ws.path ? "local_path" : "default";
|
|
2210
|
+
if (ws.mode === "local_path" && !ws.path) {
|
|
2211
|
+
ws.mode = "default";
|
|
2212
|
+
}
|
|
2213
|
+
if (ws.mode === "git") {
|
|
2214
|
+
ws.git ||= {};
|
|
2215
|
+
if (!ws.git.target_path && ws.path) {
|
|
2216
|
+
ws.git.target_path = ws.path;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (ws.mode === "default") {
|
|
2220
|
+
ws.path = void 0;
|
|
2221
|
+
ws.git = void 0;
|
|
2222
|
+
}
|
|
2223
|
+
if (ws.mode === "local_path") {
|
|
2224
|
+
ws.git = void 0;
|
|
2225
|
+
}
|
|
2226
|
+
if (ws.setup) {
|
|
2227
|
+
ws.setup.command = ws.setup.command?.trim();
|
|
2228
|
+
ws.setup.run_on ||= "first_attach";
|
|
2229
|
+
if (!ws.setup.command) {
|
|
2230
|
+
ws.setup = void 0;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
void defaultWorkspaceDir;
|
|
2234
|
+
return cfg;
|
|
2235
|
+
}
|
|
2236
|
+
function resolveWorkspaceDir(workspace, defaultWorkspaceDir) {
|
|
2237
|
+
if (workspace.mode === "local_path") {
|
|
2238
|
+
if (!workspace.path)
|
|
2239
|
+
return { workspaceDir: defaultWorkspaceDir };
|
|
2240
|
+
return {
|
|
2241
|
+
workspaceDir: requireAbsolute(workspace.path, "workspace.path"),
|
|
2242
|
+
customWorkspaceField: "workspace.path"
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
if (workspace.mode === "git") {
|
|
2246
|
+
return workspace.git?.target_path ? {
|
|
2247
|
+
workspaceDir: requireAbsolute(workspace.git.target_path, "workspace.git.target_path"),
|
|
2248
|
+
customWorkspaceField: "workspace.git.target_path"
|
|
2249
|
+
} : { workspaceDir: defaultWorkspaceDir };
|
|
2250
|
+
}
|
|
2251
|
+
return { workspaceDir: defaultWorkspaceDir };
|
|
2252
|
+
}
|
|
2253
|
+
async function ensureWorkspace(plan, log2) {
|
|
2254
|
+
const ws = plan.workspace;
|
|
2255
|
+
if (ws.mode === "default") {
|
|
2256
|
+
fs2.mkdirSync(plan.workspaceDir, { recursive: true });
|
|
2257
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (ws.mode === "local_path") {
|
|
2261
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2262
|
+
let st;
|
|
2263
|
+
try {
|
|
2264
|
+
st = fs2.statSync(plan.workspaceDir);
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
2267
|
+
throw new Error(`workspace path does not exist: ${plan.workspaceDir}`);
|
|
2268
|
+
}
|
|
2269
|
+
throw err;
|
|
2270
|
+
}
|
|
2271
|
+
if (!st.isDirectory()) {
|
|
2272
|
+
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2273
|
+
}
|
|
2274
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2275
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (ws.mode === "git") {
|
|
2279
|
+
const remote = ws.git?.remote?.trim();
|
|
2280
|
+
if (!remote)
|
|
2281
|
+
throw new Error("workspace git remote is required");
|
|
2282
|
+
if (plan.customWorkspaceField) {
|
|
2283
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2284
|
+
}
|
|
2285
|
+
if (!fs2.existsSync(plan.workspaceDir)) {
|
|
2286
|
+
fs2.mkdirSync(path3.dirname(plan.workspaceDir), { recursive: true });
|
|
2287
|
+
assertWritableWorkspaceDir(path3.dirname(plan.workspaceDir));
|
|
2288
|
+
await runCommand("git", ["clone", remote, plan.workspaceDir], process.cwd());
|
|
2289
|
+
} else {
|
|
2290
|
+
const st = fs2.statSync(plan.workspaceDir);
|
|
2291
|
+
if (!st.isDirectory()) {
|
|
2292
|
+
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2293
|
+
}
|
|
2294
|
+
if (plan.customWorkspaceField) {
|
|
2295
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2296
|
+
}
|
|
2297
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2298
|
+
await ensureGitWorktree(plan.workspaceDir);
|
|
2299
|
+
await ensureGitRemote(plan.workspaceDir, remote);
|
|
2300
|
+
}
|
|
2301
|
+
if (ws.git?.ref) {
|
|
2302
|
+
const dirty = await commandOutput("git", ["status", "--porcelain"], plan.workspaceDir);
|
|
2303
|
+
if (dirty.trim()) {
|
|
2304
|
+
throw new Error(`workspace git tree has local changes; refusing checkout: ${plan.workspaceDir}`);
|
|
2305
|
+
}
|
|
2306
|
+
await runCommand("git", ["fetch", "origin", "--prune"], plan.workspaceDir);
|
|
2307
|
+
await checkoutGitRef(plan.workspaceDir, ws.git.ref);
|
|
2308
|
+
}
|
|
2309
|
+
log2.info(`workspace ready: git ${remote} -> ${plan.workspaceDir}`);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
throw new Error(`unsupported workspace mode: ${ws.mode ?? "(empty)"}`);
|
|
2313
|
+
}
|
|
2314
|
+
async function verifyExistingWorkspace(plan, log2) {
|
|
2315
|
+
try {
|
|
2316
|
+
if (plan.customWorkspaceField) {
|
|
2317
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2318
|
+
}
|
|
2319
|
+
const st = fs2.statSync(plan.workspaceDir);
|
|
2320
|
+
if (!st.isDirectory()) {
|
|
2321
|
+
log2.warn(`workspace ready state ignored: path is not a directory: ${plan.workspaceDir}`);
|
|
2322
|
+
return false;
|
|
2323
|
+
}
|
|
2324
|
+
if (plan.customWorkspaceField) {
|
|
2325
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2326
|
+
}
|
|
2327
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2328
|
+
if (plan.workspace.mode === "git") {
|
|
2329
|
+
await ensureGitWorktree(plan.workspaceDir);
|
|
2330
|
+
const remote = plan.workspace.git?.remote?.trim();
|
|
2331
|
+
if (remote) {
|
|
2332
|
+
await ensureGitRemote(plan.workspaceDir, remote);
|
|
2333
|
+
}
|
|
2334
|
+
if (plan.workspace.git?.ref) {
|
|
2335
|
+
const head = (await commandOutput("git", ["rev-parse", "HEAD"], plan.workspaceDir)).trim();
|
|
2336
|
+
const expected = await resolveConfiguredGitRef(plan.workspaceDir, plan.workspace.git.ref);
|
|
2337
|
+
if (head !== expected) {
|
|
2338
|
+
log2.warn(`workspace ready state ignored: git HEAD ${head} does not match ${plan.workspace.git.ref} (${expected})`);
|
|
2339
|
+
return false;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return true;
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
log2.warn(`workspace ready state ignored: ${String(err)}`);
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async function ensureGitWorktree(cwd) {
|
|
2350
|
+
const result = await commandOutput("git", ["rev-parse", "--is-inside-work-tree"], cwd);
|
|
2351
|
+
if (result.trim() !== "true") {
|
|
2352
|
+
throw new Error(`workspace path exists but is not a git worktree: ${cwd}`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async function ensureGitRemote(cwd, expectedRemote) {
|
|
2356
|
+
const actualRemote = (await commandOutput("git", ["remote", "get-url", "origin"], cwd)).trim();
|
|
2357
|
+
if (actualRemote !== expectedRemote) {
|
|
2358
|
+
throw new Error(`workspace git remote mismatch: expected ${expectedRemote}, got ${actualRemote}`);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
async function checkoutGitRef(cwd, ref) {
|
|
2362
|
+
const remoteCommit = await resolveRemoteGitRef(cwd, ref);
|
|
2363
|
+
if (remoteCommit) {
|
|
2364
|
+
await runCommand("git", ["checkout", "-B", ref, remoteCommit], cwd);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const targetCommit = await resolveGitCommit(cwd, ref);
|
|
2368
|
+
await runCommand("git", ["checkout", "--detach", targetCommit], cwd);
|
|
2369
|
+
}
|
|
2370
|
+
async function resolveConfiguredGitRef(cwd, ref) {
|
|
2371
|
+
const remote = await resolveRemoteGitRef(cwd, ref);
|
|
2372
|
+
return remote || await resolveGitCommit(cwd, ref);
|
|
2373
|
+
}
|
|
2374
|
+
async function resolveRemoteGitRef(cwd, ref) {
|
|
2375
|
+
return (await tryGitOutput("git", ["rev-parse", "--verify", `refs/remotes/origin/${ref}^{commit}`], cwd)).trim();
|
|
2376
|
+
}
|
|
2377
|
+
async function resolveGitCommit(cwd, ref) {
|
|
2378
|
+
const candidates = [`refs/tags/${ref}^{commit}`, `${ref}^{commit}`];
|
|
2379
|
+
for (const candidate of candidates) {
|
|
2380
|
+
const commit = await tryGitOutput("git", ["rev-parse", "--verify", candidate], cwd);
|
|
2381
|
+
if (commit.trim()) {
|
|
2382
|
+
return commit.trim();
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
throw new Error(`workspace git ref not found: ${ref}`);
|
|
2386
|
+
}
|
|
2387
|
+
function shouldRunSetup(setup, prior, configHash, forceSetup = false) {
|
|
2388
|
+
if (!setup?.command)
|
|
2389
|
+
return false;
|
|
2390
|
+
if (forceSetup)
|
|
2391
|
+
return true;
|
|
2392
|
+
if (setup.run_on === "config_change") {
|
|
2393
|
+
return prior?.config_hash !== configHash || prior.status === "pending" || prior.status === "failed";
|
|
2394
|
+
}
|
|
2395
|
+
return prior?.status !== "ready" || !prior.prepared_at;
|
|
2396
|
+
}
|
|
2397
|
+
async function runSetup(cwd, command, timeoutSec) {
|
|
2398
|
+
const effectiveTimeoutSec = timeoutSec && timeoutSec > 0 ? timeoutSec : DEFAULT_SETUP_TIMEOUT_SEC;
|
|
2399
|
+
return runCommand(process.env.SHELL || "/bin/sh", ["-lc", command], cwd, effectiveTimeoutSec * 1e3, scrubSetupEnv());
|
|
2400
|
+
}
|
|
2401
|
+
function scrubSetupEnv() {
|
|
2402
|
+
const env = { ...process.env };
|
|
2403
|
+
clearAllProviderCreds(env);
|
|
2404
|
+
delete env.PRLL_API_KEY;
|
|
2405
|
+
return env;
|
|
2406
|
+
}
|
|
2407
|
+
async function commandOutput(cmd, args, cwd) {
|
|
2408
|
+
return runCommand(cmd, args, cwd);
|
|
2409
|
+
}
|
|
2410
|
+
async function tryGitOutput(cmd, args, cwd) {
|
|
2411
|
+
try {
|
|
2412
|
+
return await runCommand(cmd, args, cwd);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return "";
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
|
|
2418
|
+
return new Promise((resolve4, reject) => {
|
|
2419
|
+
let tail = "";
|
|
2420
|
+
let timedOut = false;
|
|
2421
|
+
let settled = false;
|
|
2422
|
+
let timeoutTimer = null;
|
|
2423
|
+
let killTimer = null;
|
|
2424
|
+
const append = (chunk) => {
|
|
2425
|
+
tail += chunk.toString();
|
|
2426
|
+
if (tail.length > OUTPUT_TAIL_LIMIT) {
|
|
2427
|
+
tail = tail.slice(-OUTPUT_TAIL_LIMIT);
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
const settle = (fn) => {
|
|
2431
|
+
if (settled)
|
|
2432
|
+
return;
|
|
2433
|
+
settled = true;
|
|
2434
|
+
if (timeoutTimer)
|
|
2435
|
+
clearTimeout(timeoutTimer);
|
|
2436
|
+
if (killTimer)
|
|
2437
|
+
clearTimeout(killTimer);
|
|
2438
|
+
fn();
|
|
2439
|
+
};
|
|
2440
|
+
const child = spawn(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2441
|
+
timeoutTimer = setTimeout(() => {
|
|
2442
|
+
timedOut = true;
|
|
2443
|
+
child.kill("SIGTERM");
|
|
2444
|
+
killTimer = setTimeout(() => {
|
|
2445
|
+
child.kill("SIGKILL");
|
|
2446
|
+
}, 5e3);
|
|
2447
|
+
}, timeoutMs);
|
|
2448
|
+
child.stdout?.on("data", append);
|
|
2449
|
+
child.stderr?.on("data", append);
|
|
2450
|
+
child.once("error", (err) => {
|
|
2451
|
+
settle(() => reject(err));
|
|
2452
|
+
});
|
|
2453
|
+
child.once("close", (code, signal) => {
|
|
2454
|
+
if (timedOut) {
|
|
2455
|
+
settle(() => reject(new Error(`command timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}
|
|
2456
|
+
${tail}`)));
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
if (code === 0) {
|
|
2460
|
+
settle(() => resolve4(tail));
|
|
2461
|
+
} else {
|
|
2462
|
+
settle(() => reject(new Error(`command failed (${code ?? signal}): ${cmd} ${args.join(" ")}
|
|
2463
|
+
${tail}`)));
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function requireAbsolute(value, field) {
|
|
2469
|
+
if (!value || !path3.isAbsolute(value)) {
|
|
2470
|
+
throw new Error(`${field} must be an absolute path`);
|
|
2471
|
+
}
|
|
2472
|
+
return path3.resolve(value);
|
|
2473
|
+
}
|
|
2474
|
+
function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
|
|
2475
|
+
if (!plan.customWorkspaceField)
|
|
2476
|
+
return;
|
|
2477
|
+
const normalized = requireAbsolute(candidate, plan.customWorkspaceField);
|
|
2478
|
+
const reason = workspacePathDenyReason(toPolicyPath(normalized));
|
|
2479
|
+
if (reason) {
|
|
2480
|
+
throw new Error(`${plan.customWorkspaceField} must point to a project folder, not ${reason}: ${normalized}`);
|
|
2481
|
+
}
|
|
2482
|
+
const defaultWorkspace = path3.resolve(plan.defaultWorkspaceDir);
|
|
2483
|
+
if (isAncestorPath(normalized, defaultWorkspace) || normalized === defaultWorkspace) {
|
|
2484
|
+
throw new Error(`${plan.customWorkspaceField} must not point at daemon state directories: ${normalized}`);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
function assertWritableWorkspaceDir(dir) {
|
|
2488
|
+
fs2.accessSync(dir, fs2.constants.R_OK | fs2.constants.W_OK | fs2.constants.X_OK);
|
|
2489
|
+
const probe = path3.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
|
|
2490
|
+
const fd = fs2.openSync(probe, "wx", 384);
|
|
2491
|
+
fs2.closeSync(fd);
|
|
2492
|
+
fs2.unlinkSync(probe);
|
|
2493
|
+
}
|
|
2494
|
+
function workspacePathDenyReason(value) {
|
|
2495
|
+
if (value === "/")
|
|
2496
|
+
return "the filesystem root";
|
|
2497
|
+
if (["/tmp", "/private/tmp", "/var/tmp", "/Users", "/home"].includes(value)) {
|
|
2498
|
+
return "a shared or home root directory";
|
|
2499
|
+
}
|
|
2500
|
+
for (const root of [
|
|
2501
|
+
"/Applications",
|
|
2502
|
+
"/bin",
|
|
2503
|
+
"/boot",
|
|
2504
|
+
"/dev",
|
|
2505
|
+
"/etc",
|
|
2506
|
+
"/Library",
|
|
2507
|
+
"/private/etc",
|
|
2508
|
+
"/private/var/db",
|
|
2509
|
+
"/proc",
|
|
2510
|
+
"/root",
|
|
2511
|
+
"/run",
|
|
2512
|
+
"/sbin",
|
|
2513
|
+
"/System",
|
|
2514
|
+
"/sys",
|
|
2515
|
+
"/usr",
|
|
2516
|
+
"/var/db",
|
|
2517
|
+
"/var/root"
|
|
2518
|
+
]) {
|
|
2519
|
+
if (value === root || value.startsWith(`${root}/`)) {
|
|
2520
|
+
return "a system directory";
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
const parts = value.split("/").filter(Boolean);
|
|
2524
|
+
if (parts.includes(".git"))
|
|
2525
|
+
return "a git metadata directory";
|
|
2526
|
+
if (parts.length === 2 && (parts[0] === "Users" || parts[0] === "home")) {
|
|
2527
|
+
return "a home directory";
|
|
2528
|
+
}
|
|
2529
|
+
if (parts.length >= 3 && (parts[0] === "Users" || parts[0] === "home")) {
|
|
2530
|
+
const homeChild = parts[2];
|
|
2531
|
+
if ([
|
|
2532
|
+
".aws",
|
|
2533
|
+
".azure",
|
|
2534
|
+
".claude",
|
|
2535
|
+
".codex",
|
|
2536
|
+
".config",
|
|
2537
|
+
".docker",
|
|
2538
|
+
".gnupg",
|
|
2539
|
+
".kube",
|
|
2540
|
+
".local",
|
|
2541
|
+
".npm",
|
|
2542
|
+
".ssh",
|
|
2543
|
+
".parall-agent",
|
|
2544
|
+
".parall-daemon",
|
|
2545
|
+
"Library"
|
|
2546
|
+
].includes(homeChild)) {
|
|
2547
|
+
return "a credential or application state directory";
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return "";
|
|
2551
|
+
}
|
|
2552
|
+
function isAncestorPath(parent, child) {
|
|
2553
|
+
const relative2 = path3.relative(parent, child);
|
|
2554
|
+
return relative2 !== "" && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
|
|
2555
|
+
}
|
|
2556
|
+
function toPolicyPath(value) {
|
|
2557
|
+
return path3.resolve(value).split(path3.sep).join("/");
|
|
2558
|
+
}
|
|
2559
|
+
function isNodeError(err) {
|
|
2560
|
+
return err instanceof Error && "code" in err;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2061
2563
|
// ts/daemon/dist/supervisor.js
|
|
2062
2564
|
var RUNTIME_PACKAGES = {
|
|
2063
2565
|
"claude-code": "@parall/claude-agent",
|
|
2064
2566
|
"codex": "@parall/codex-agent",
|
|
2065
2567
|
"openclaw": "@parall/openclaw-agent"
|
|
2066
2568
|
};
|
|
2569
|
+
var WORKSPACE_SETUP_RETRY_DELAY_MS = 5e3;
|
|
2067
2570
|
function sleepCancellable(ms, signal) {
|
|
2068
2571
|
if (signal.aborted)
|
|
2069
2572
|
return Promise.resolve(false);
|
|
2070
|
-
return new Promise((
|
|
2573
|
+
return new Promise((resolve4) => {
|
|
2071
2574
|
const timer = setTimeout(() => {
|
|
2072
2575
|
signal.removeEventListener("abort", onAbort);
|
|
2073
|
-
|
|
2576
|
+
resolve4(true);
|
|
2074
2577
|
}, ms);
|
|
2075
2578
|
const onAbort = () => {
|
|
2076
2579
|
clearTimeout(timer);
|
|
2077
|
-
|
|
2580
|
+
resolve4(false);
|
|
2078
2581
|
};
|
|
2079
2582
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2080
2583
|
});
|
|
@@ -2084,6 +2587,10 @@ var DaemonSupervisor = class {
|
|
|
2084
2587
|
client;
|
|
2085
2588
|
log;
|
|
2086
2589
|
children = /* @__PURE__ */ new Map();
|
|
2590
|
+
spawningAgents = /* @__PURE__ */ new Set();
|
|
2591
|
+
pendingWorkspaceSetup = /* @__PURE__ */ new Set();
|
|
2592
|
+
workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
|
|
2593
|
+
cancelledSpawns = /* @__PURE__ */ new Set();
|
|
2087
2594
|
ws = null;
|
|
2088
2595
|
running = false;
|
|
2089
2596
|
machineOrgId = null;
|
|
@@ -2144,6 +2651,10 @@ var DaemonSupervisor = class {
|
|
|
2144
2651
|
void this.respawnAllChildren();
|
|
2145
2652
|
}
|
|
2146
2653
|
});
|
|
2654
|
+
this.ws.on("machine.workspace.setup.requested", (data) => {
|
|
2655
|
+
this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
|
|
2656
|
+
void this.handleWorkspaceSetupRequested(data.agent_id);
|
|
2657
|
+
});
|
|
2147
2658
|
this.ws.on("machine.stop", (data) => {
|
|
2148
2659
|
this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
|
|
2149
2660
|
void this.stop();
|
|
@@ -2154,8 +2665,8 @@ var DaemonSupervisor = class {
|
|
|
2154
2665
|
}
|
|
2155
2666
|
});
|
|
2156
2667
|
await this.ws.connect();
|
|
2157
|
-
await new Promise((
|
|
2158
|
-
this.stopResolve =
|
|
2668
|
+
await new Promise((resolve4) => {
|
|
2669
|
+
this.stopResolve = resolve4;
|
|
2159
2670
|
});
|
|
2160
2671
|
signal.removeEventListener("abort", onAbort);
|
|
2161
2672
|
}
|
|
@@ -2169,6 +2680,10 @@ var DaemonSupervisor = class {
|
|
|
2169
2680
|
this.ws = null;
|
|
2170
2681
|
}
|
|
2171
2682
|
const exits = [];
|
|
2683
|
+
for (const timer of this.workspaceSetupRetryTimers.values()) {
|
|
2684
|
+
clearTimeout(timer);
|
|
2685
|
+
}
|
|
2686
|
+
this.workspaceSetupRetryTimers.clear();
|
|
2172
2687
|
for (const state of this.children.values()) {
|
|
2173
2688
|
state.shuttingDown = true;
|
|
2174
2689
|
if (state.restartTimer) {
|
|
@@ -2270,15 +2785,15 @@ var DaemonSupervisor = class {
|
|
|
2270
2785
|
*/
|
|
2271
2786
|
migrateFlatLayout() {
|
|
2272
2787
|
const root = this.config.rootStateDir;
|
|
2273
|
-
const agentsDir =
|
|
2274
|
-
const flatWorkspace =
|
|
2275
|
-
if (!
|
|
2788
|
+
const agentsDir = path4.join(root, "agents");
|
|
2789
|
+
const flatWorkspace = path4.join(root, "workspace");
|
|
2790
|
+
if (!fs3.existsSync(flatWorkspace) || fs3.existsSync(agentsDir))
|
|
2276
2791
|
return;
|
|
2277
2792
|
let ownerAgentId;
|
|
2278
|
-
const sessionsDir =
|
|
2279
|
-
if (
|
|
2793
|
+
const sessionsDir = path4.join(root, "sessions");
|
|
2794
|
+
if (fs3.existsSync(sessionsDir)) {
|
|
2280
2795
|
try {
|
|
2281
|
-
for (const file of
|
|
2796
|
+
for (const file of fs3.readdirSync(sessionsDir)) {
|
|
2282
2797
|
if (!file.endsWith(".json"))
|
|
2283
2798
|
continue;
|
|
2284
2799
|
const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
|
|
@@ -2292,13 +2807,13 @@ var DaemonSupervisor = class {
|
|
|
2292
2807
|
}
|
|
2293
2808
|
}
|
|
2294
2809
|
const targetId = ownerAgentId ?? "_orphan";
|
|
2295
|
-
const targetDir =
|
|
2810
|
+
const targetDir = path4.join(agentsDir, targetId);
|
|
2296
2811
|
try {
|
|
2297
|
-
|
|
2812
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
2298
2813
|
for (const sub of ["workspace", "sessions", "dispatch-context"]) {
|
|
2299
|
-
const src =
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2814
|
+
const src = path4.join(root, sub);
|
|
2815
|
+
if (fs3.existsSync(src)) {
|
|
2816
|
+
fs3.renameSync(src, path4.join(targetDir, sub));
|
|
2302
2817
|
}
|
|
2303
2818
|
}
|
|
2304
2819
|
this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
|
|
@@ -2307,23 +2822,30 @@ var DaemonSupervisor = class {
|
|
|
2307
2822
|
}
|
|
2308
2823
|
}
|
|
2309
2824
|
// ---- WS event handlers (incremental) ----
|
|
2310
|
-
async
|
|
2311
|
-
if (this.children.has(agentId))
|
|
2312
|
-
return;
|
|
2825
|
+
async fetchAttachedAgent(agentId) {
|
|
2313
2826
|
let attached;
|
|
2314
2827
|
try {
|
|
2315
2828
|
attached = await this.client.listAttachedAgents();
|
|
2316
2829
|
} catch (err) {
|
|
2317
|
-
this.log.warn(`
|
|
2318
|
-
return;
|
|
2830
|
+
this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
|
|
2831
|
+
return { kind: "retryable" };
|
|
2319
2832
|
}
|
|
2320
2833
|
const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
|
|
2321
2834
|
if (!entry) {
|
|
2322
|
-
this.log.warn(`
|
|
2323
|
-
return;
|
|
2835
|
+
this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
|
|
2836
|
+
return { kind: "skip" };
|
|
2324
2837
|
}
|
|
2325
2838
|
if (entry.user && entry.user.status !== "active") {
|
|
2326
2839
|
this.log.info(`agent ${agentId} not active (status=${entry.user.status}) \u2014 skipping`);
|
|
2840
|
+
return { kind: "skip" };
|
|
2841
|
+
}
|
|
2842
|
+
return { kind: "found", entry };
|
|
2843
|
+
}
|
|
2844
|
+
async handleAgentAttached(agentId) {
|
|
2845
|
+
if (this.children.has(agentId) || this.spawningAgents.has(agentId))
|
|
2846
|
+
return;
|
|
2847
|
+
const result = await this.fetchAttachedAgent(agentId);
|
|
2848
|
+
if (result.kind !== "found") {
|
|
2327
2849
|
return;
|
|
2328
2850
|
}
|
|
2329
2851
|
const orgId = this.machineOrgId;
|
|
@@ -2331,9 +2853,14 @@ var DaemonSupervisor = class {
|
|
|
2331
2853
|
this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
|
|
2332
2854
|
return;
|
|
2333
2855
|
}
|
|
2334
|
-
await this.spawnAgent(agentId, orgId, entry);
|
|
2856
|
+
await this.spawnAgent(agentId, orgId, result.entry);
|
|
2335
2857
|
}
|
|
2336
2858
|
async handleAgentDetached(agentId) {
|
|
2859
|
+
this.pendingWorkspaceSetup.delete(agentId);
|
|
2860
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2861
|
+
if (this.spawningAgents.has(agentId)) {
|
|
2862
|
+
this.cancelledSpawns.add(agentId);
|
|
2863
|
+
}
|
|
2337
2864
|
const state = this.children.get(agentId);
|
|
2338
2865
|
if (!state)
|
|
2339
2866
|
return;
|
|
@@ -2346,6 +2873,61 @@ var DaemonSupervisor = class {
|
|
|
2346
2873
|
await this.terminateChild(state);
|
|
2347
2874
|
this.children.delete(agentId);
|
|
2348
2875
|
}
|
|
2876
|
+
async handleWorkspaceSetupRequested(agentId) {
|
|
2877
|
+
if (this.spawningAgents.has(agentId)) {
|
|
2878
|
+
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|
|
2879
|
+
this.pendingWorkspaceSetup.add(agentId);
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const result = await this.fetchAttachedAgent(agentId);
|
|
2883
|
+
if (result.kind === "retryable") {
|
|
2884
|
+
this.pendingWorkspaceSetup.add(agentId);
|
|
2885
|
+
this.scheduleWorkspaceSetupRetry(agentId);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (result.kind === "skip") {
|
|
2889
|
+
this.pendingWorkspaceSetup.delete(agentId);
|
|
2890
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2894
|
+
const state = this.children.get(agentId);
|
|
2895
|
+
if (state) {
|
|
2896
|
+
this.log.info(`agent ${agentId}: restarting for workspace setup`);
|
|
2897
|
+
state.shuttingDown = true;
|
|
2898
|
+
if (state.restartTimer) {
|
|
2899
|
+
clearTimeout(state.restartTimer);
|
|
2900
|
+
state.restartTimer = null;
|
|
2901
|
+
}
|
|
2902
|
+
await this.terminateChild(state);
|
|
2903
|
+
this.children.delete(agentId);
|
|
2904
|
+
}
|
|
2905
|
+
const orgId = this.machineOrgId;
|
|
2906
|
+
if (!orgId) {
|
|
2907
|
+
this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
await this.spawnAgent(agentId, orgId, result.entry);
|
|
2911
|
+
}
|
|
2912
|
+
scheduleWorkspaceSetupRetry(agentId) {
|
|
2913
|
+
if (!this.running || this.workspaceSetupRetryTimers.has(agentId))
|
|
2914
|
+
return;
|
|
2915
|
+
const timer = setTimeout(() => {
|
|
2916
|
+
this.workspaceSetupRetryTimers.delete(agentId);
|
|
2917
|
+
if (this.running && this.pendingWorkspaceSetup.has(agentId)) {
|
|
2918
|
+
void this.handleWorkspaceSetupRequested(agentId);
|
|
2919
|
+
}
|
|
2920
|
+
}, WORKSPACE_SETUP_RETRY_DELAY_MS);
|
|
2921
|
+
timer.unref?.();
|
|
2922
|
+
this.workspaceSetupRetryTimers.set(agentId, timer);
|
|
2923
|
+
}
|
|
2924
|
+
clearWorkspaceSetupRetry(agentId) {
|
|
2925
|
+
const timer = this.workspaceSetupRetryTimers.get(agentId);
|
|
2926
|
+
if (timer) {
|
|
2927
|
+
clearTimeout(timer);
|
|
2928
|
+
this.workspaceSetupRetryTimers.delete(agentId);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2349
2931
|
// ---- Spawn / restart ----
|
|
2350
2932
|
async refreshMachineConfig() {
|
|
2351
2933
|
try {
|
|
@@ -2385,6 +2967,25 @@ var DaemonSupervisor = class {
|
|
|
2385
2967
|
}
|
|
2386
2968
|
}
|
|
2387
2969
|
async spawnAgent(agentId, orgId, attached) {
|
|
2970
|
+
if (this.children.has(agentId) || this.spawningAgents.has(agentId)) {
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
this.spawningAgents.add(agentId);
|
|
2974
|
+
let shouldReplaySetupRequest = false;
|
|
2975
|
+
try {
|
|
2976
|
+
await this.spawnAgentOnce(agentId, orgId, attached);
|
|
2977
|
+
} finally {
|
|
2978
|
+
this.spawningAgents.delete(agentId);
|
|
2979
|
+
this.cancelledSpawns.delete(agentId);
|
|
2980
|
+
shouldReplaySetupRequest = this.pendingWorkspaceSetup.delete(agentId);
|
|
2981
|
+
}
|
|
2982
|
+
if (shouldReplaySetupRequest && this.running) {
|
|
2983
|
+
queueMicrotask(() => {
|
|
2984
|
+
void this.handleWorkspaceSetupRequested(agentId);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
async spawnAgentOnce(agentId, orgId, attached) {
|
|
2388
2989
|
let credential;
|
|
2389
2990
|
try {
|
|
2390
2991
|
credential = await this.client.mintLaunchCredential(agentId);
|
|
@@ -2393,22 +2994,36 @@ var DaemonSupervisor = class {
|
|
|
2393
2994
|
return;
|
|
2394
2995
|
}
|
|
2395
2996
|
const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
|
|
2396
|
-
const
|
|
2997
|
+
const defaultWorkspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
|
|
2397
2998
|
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
2398
2999
|
const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
|
|
2399
3000
|
try {
|
|
2400
|
-
|
|
2401
|
-
if (!attached.daemon_config?.workspace_path) {
|
|
2402
|
-
fs2.mkdirSync(workspaceDir, { recursive: true });
|
|
2403
|
-
}
|
|
3001
|
+
fs3.mkdirSync(stateDir, { recursive: true });
|
|
2404
3002
|
if (isK8s) {
|
|
2405
|
-
|
|
3003
|
+
fs3.mkdirSync(claudeHome, { recursive: true });
|
|
2406
3004
|
this.ensureSharedCredentialLink(claudeHome, agentId);
|
|
2407
3005
|
}
|
|
2408
3006
|
} catch (err) {
|
|
2409
3007
|
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
2410
3008
|
}
|
|
2411
3009
|
const runtimeType = attached.profile.runtime_type ?? "claude-code";
|
|
3010
|
+
let workspaceDir;
|
|
3011
|
+
try {
|
|
3012
|
+
workspaceDir = await prepareWorkspace({
|
|
3013
|
+
client: this.client,
|
|
3014
|
+
attached,
|
|
3015
|
+
agentId,
|
|
3016
|
+
defaultWorkspaceDir,
|
|
3017
|
+
log: this.log
|
|
3018
|
+
});
|
|
3019
|
+
} catch (err) {
|
|
3020
|
+
this.log.error(`agent ${agentId}: workspace setup failed: ${String(err)}`);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (this.cancelledSpawns.delete(agentId)) {
|
|
3024
|
+
this.log.info(`agent ${agentId}: spawn cancelled after detach during workspace setup`);
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
2412
3027
|
const state = {
|
|
2413
3028
|
agentId,
|
|
2414
3029
|
orgId,
|
|
@@ -2441,7 +3056,7 @@ var DaemonSupervisor = class {
|
|
|
2441
3056
|
};
|
|
2442
3057
|
const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
|
|
2443
3058
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
|
|
2444
|
-
const child =
|
|
3059
|
+
const child = spawn2(adapter.bin, [], {
|
|
2445
3060
|
env,
|
|
2446
3061
|
stdio: ["ignore", "inherit", "inherit"],
|
|
2447
3062
|
detached: false
|
|
@@ -2479,25 +3094,26 @@ var DaemonSupervisor = class {
|
|
|
2479
3094
|
settleChild("error", null, null, err);
|
|
2480
3095
|
});
|
|
2481
3096
|
child.once("close", (code, signal) => settleChild("close", code, signal));
|
|
2482
|
-
setTimeout(() => {
|
|
3097
|
+
const stableTimer = setTimeout(() => {
|
|
2483
3098
|
if (state.child === child) {
|
|
2484
3099
|
state.restartAttempts = 0;
|
|
2485
3100
|
}
|
|
2486
3101
|
}, Math.max(this.config.restartBackoffMs, 3e4));
|
|
3102
|
+
stableTimer.unref?.();
|
|
2487
3103
|
}
|
|
2488
3104
|
async terminateChild(state) {
|
|
2489
3105
|
const child = state.child;
|
|
2490
3106
|
if (!child)
|
|
2491
3107
|
return;
|
|
2492
|
-
return new Promise((
|
|
2493
|
-
const onExit = () =>
|
|
3108
|
+
return new Promise((resolve4) => {
|
|
3109
|
+
const onExit = () => resolve4();
|
|
2494
3110
|
child.once("exit", onExit);
|
|
2495
3111
|
try {
|
|
2496
3112
|
child.kill("SIGTERM");
|
|
2497
3113
|
} catch (err) {
|
|
2498
3114
|
this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
|
|
2499
3115
|
child.off("exit", onExit);
|
|
2500
|
-
|
|
3116
|
+
resolve4();
|
|
2501
3117
|
return;
|
|
2502
3118
|
}
|
|
2503
3119
|
const hardKill = setTimeout(() => {
|
|
@@ -2510,60 +3126,60 @@ var DaemonSupervisor = class {
|
|
|
2510
3126
|
});
|
|
2511
3127
|
}
|
|
2512
3128
|
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
2513
|
-
const sharedCredentials =
|
|
3129
|
+
const sharedCredentials = path4.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
|
|
2514
3130
|
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
2515
|
-
const agentCredentialsDir =
|
|
2516
|
-
|
|
2517
|
-
|
|
3131
|
+
const agentCredentialsDir = path4.dirname(agentCredentials);
|
|
3132
|
+
fs3.mkdirSync(path4.dirname(sharedCredentials), { recursive: true });
|
|
3133
|
+
fs3.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
2518
3134
|
try {
|
|
2519
|
-
const existing =
|
|
3135
|
+
const existing = fs3.lstatSync(agentCredentials);
|
|
2520
3136
|
if (existing.isSymbolicLink()) {
|
|
2521
|
-
const currentTarget =
|
|
2522
|
-
if (
|
|
3137
|
+
const currentTarget = fs3.readlinkSync(agentCredentials);
|
|
3138
|
+
if (path4.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
2523
3139
|
return;
|
|
2524
3140
|
}
|
|
2525
|
-
|
|
3141
|
+
fs3.unlinkSync(agentCredentials);
|
|
2526
3142
|
} else if (existing.isDirectory()) {
|
|
2527
3143
|
this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
2528
3144
|
return;
|
|
2529
3145
|
} else {
|
|
2530
|
-
|
|
3146
|
+
fs3.unlinkSync(agentCredentials);
|
|
2531
3147
|
}
|
|
2532
3148
|
} catch (err) {
|
|
2533
3149
|
if (err.code !== "ENOENT") {
|
|
2534
3150
|
throw err;
|
|
2535
3151
|
}
|
|
2536
3152
|
}
|
|
2537
|
-
|
|
3153
|
+
fs3.symlinkSync(sharedCredentials, agentCredentials);
|
|
2538
3154
|
}
|
|
2539
3155
|
};
|
|
2540
3156
|
|
|
2541
3157
|
// ts/daemon/dist/cli.js
|
|
2542
|
-
import * as
|
|
2543
|
-
import * as
|
|
3158
|
+
import * as fs4 from "node:fs";
|
|
3159
|
+
import * as path5 from "node:path";
|
|
2544
3160
|
import * as os2 from "node:os";
|
|
2545
3161
|
import * as readline from "node:readline";
|
|
2546
|
-
import { spawn as
|
|
3162
|
+
import { spawn as spawn3, execSync } from "node:child_process";
|
|
2547
3163
|
var CONFIG_DIR = daemonConfigDir();
|
|
2548
3164
|
var CONFIG_PATH = daemonConfigPath();
|
|
2549
3165
|
function readConfig() {
|
|
2550
3166
|
try {
|
|
2551
|
-
return JSON.parse(
|
|
3167
|
+
return JSON.parse(fs4.readFileSync(CONFIG_PATH, "utf-8"));
|
|
2552
3168
|
} catch {
|
|
2553
3169
|
return null;
|
|
2554
3170
|
}
|
|
2555
3171
|
}
|
|
2556
3172
|
function writeConfig(config) {
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
3173
|
+
fs4.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3174
|
+
fs4.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3175
|
+
fs4.chmodSync(CONFIG_PATH, 384);
|
|
2560
3176
|
}
|
|
2561
3177
|
function prompt(question) {
|
|
2562
3178
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2563
|
-
return new Promise((
|
|
3179
|
+
return new Promise((resolve4) => {
|
|
2564
3180
|
rl.question(question, (answer) => {
|
|
2565
3181
|
rl.close();
|
|
2566
|
-
|
|
3182
|
+
resolve4(answer.trim());
|
|
2567
3183
|
});
|
|
2568
3184
|
});
|
|
2569
3185
|
}
|
|
@@ -2575,10 +3191,10 @@ function isLinux() {
|
|
|
2575
3191
|
}
|
|
2576
3192
|
var PLIST_LABEL = "com.parall.daemon";
|
|
2577
3193
|
function plistPath() {
|
|
2578
|
-
return
|
|
3194
|
+
return path5.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2579
3195
|
}
|
|
2580
3196
|
function systemdUnitPath() {
|
|
2581
|
-
return
|
|
3197
|
+
return path5.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
|
|
2582
3198
|
}
|
|
2583
3199
|
function getDaemonBin() {
|
|
2584
3200
|
try {
|
|
@@ -2588,7 +3204,7 @@ function getDaemonBin() {
|
|
|
2588
3204
|
}
|
|
2589
3205
|
}
|
|
2590
3206
|
function generatePlist(daemonBin) {
|
|
2591
|
-
const logPath =
|
|
3207
|
+
const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
2592
3208
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2593
3209
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2594
3210
|
<plist version="1.0">
|
|
@@ -2635,16 +3251,16 @@ function installService() {
|
|
|
2635
3251
|
}
|
|
2636
3252
|
const bin = getDaemonBin();
|
|
2637
3253
|
if (isMacOS()) {
|
|
2638
|
-
const dir =
|
|
2639
|
-
|
|
2640
|
-
|
|
3254
|
+
const dir = path5.dirname(plistPath());
|
|
3255
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3256
|
+
fs4.writeFileSync(plistPath(), generatePlist(bin));
|
|
2641
3257
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2642
3258
|
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
|
|
2643
3259
|
console.log(`launchd agent installed: ${plistPath()}`);
|
|
2644
3260
|
} else if (isLinux()) {
|
|
2645
|
-
const dir =
|
|
2646
|
-
|
|
2647
|
-
|
|
3261
|
+
const dir = path5.dirname(systemdUnitPath());
|
|
3262
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3263
|
+
fs4.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
|
|
2648
3264
|
execSync("systemctl --user daemon-reload");
|
|
2649
3265
|
execSync("systemctl --user enable --now parall-daemon");
|
|
2650
3266
|
console.log(`systemd service installed: ${systemdUnitPath()}`);
|
|
@@ -2705,31 +3321,31 @@ function cmdStop() {
|
|
|
2705
3321
|
}
|
|
2706
3322
|
function cmdLogs(lines) {
|
|
2707
3323
|
if (isLinux()) {
|
|
2708
|
-
const child2 =
|
|
3324
|
+
const child2 = spawn3("journalctl", ["--user-unit", "parall-daemon", "-n", lines, "-f"], {
|
|
2709
3325
|
stdio: "inherit"
|
|
2710
3326
|
});
|
|
2711
3327
|
child2.on("exit", (code) => process.exit(code ?? 0));
|
|
2712
3328
|
return;
|
|
2713
3329
|
}
|
|
2714
|
-
const logPath =
|
|
2715
|
-
if (!
|
|
3330
|
+
const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
3331
|
+
if (!fs4.existsSync(logPath)) {
|
|
2716
3332
|
console.log("No log file found at", logPath);
|
|
2717
3333
|
return;
|
|
2718
3334
|
}
|
|
2719
|
-
const child =
|
|
3335
|
+
const child = spawn3("tail", ["-n", lines, "-f", logPath], { stdio: "inherit" });
|
|
2720
3336
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2721
3337
|
}
|
|
2722
3338
|
function cmdServiceUninstall() {
|
|
2723
3339
|
if (isMacOS()) {
|
|
2724
3340
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2725
|
-
if (
|
|
2726
|
-
|
|
3341
|
+
if (fs4.existsSync(plistPath()))
|
|
3342
|
+
fs4.unlinkSync(plistPath());
|
|
2727
3343
|
console.log("launchd agent uninstalled.");
|
|
2728
3344
|
} else if (isLinux()) {
|
|
2729
3345
|
execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
|
|
2730
3346
|
execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
|
|
2731
|
-
if (
|
|
2732
|
-
|
|
3347
|
+
if (fs4.existsSync(systemdUnitPath()))
|
|
3348
|
+
fs4.unlinkSync(systemdUnitPath());
|
|
2733
3349
|
execSync("systemctl --user daemon-reload");
|
|
2734
3350
|
console.log("systemd service uninstalled.");
|
|
2735
3351
|
} else {
|
|
@@ -2846,7 +3462,8 @@ async function main() {
|
|
|
2846
3462
|
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
2847
3463
|
const client = new ParallClient({
|
|
2848
3464
|
baseUrl: config.apiUrl,
|
|
2849
|
-
token: config.apiKey
|
|
3465
|
+
token: config.apiKey,
|
|
3466
|
+
swimlaneName: config.swimlaneName
|
|
2850
3467
|
});
|
|
2851
3468
|
const abortController = new AbortController();
|
|
2852
3469
|
const onSignal = (sig) => {
|
|
@@ -2863,7 +3480,7 @@ async function main() {
|
|
|
2863
3480
|
process.exitCode = 1;
|
|
2864
3481
|
process.exit(1);
|
|
2865
3482
|
});
|
|
2866
|
-
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
|
|
3483
|
+
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
2867
3484
|
await runForever(config, client, log, abortController.signal);
|
|
2868
3485
|
}
|
|
2869
3486
|
var cliArgs = process.argv.slice(2);
|