@parall/daemon 1.29.0 → 1.29.2
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 +726 -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 +439 -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,451 @@ 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
|
+
if (!opts.attached.daemon_config && !prior) {
|
|
2150
|
+
return plan.workspaceDir;
|
|
2151
|
+
}
|
|
2152
|
+
let forceSetup = false;
|
|
2153
|
+
if (prior?.status === "ready" && prior.config_hash === plan.configHash) {
|
|
2154
|
+
if (await verifyExistingWorkspace(plan, opts.log)) {
|
|
2155
|
+
return plan.workspaceDir;
|
|
2156
|
+
}
|
|
2157
|
+
forceSetup = true;
|
|
2158
|
+
}
|
|
2159
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2160
|
+
config_hash: plan.configHash,
|
|
2161
|
+
status: "preparing"
|
|
2162
|
+
});
|
|
2163
|
+
let outputTail = "";
|
|
2164
|
+
try {
|
|
2165
|
+
await ensureWorkspace(plan, opts.log);
|
|
2166
|
+
if (shouldRunSetup(plan.workspace.setup, prior, plan.configHash, forceSetup)) {
|
|
2167
|
+
outputTail = await runSetup(plan.workspaceDir, plan.workspace.setup.command, plan.workspace.setup?.timeout_sec);
|
|
2168
|
+
}
|
|
2169
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2170
|
+
config_hash: plan.configHash,
|
|
2171
|
+
status: "ready",
|
|
2172
|
+
output_tail: outputTail || null
|
|
2173
|
+
});
|
|
2174
|
+
return plan.workspaceDir;
|
|
2175
|
+
} catch (err) {
|
|
2176
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2177
|
+
await opts.client.reportAgentWorkspaceState(opts.agentId, {
|
|
2178
|
+
config_hash: plan.configHash,
|
|
2179
|
+
status: "failed",
|
|
2180
|
+
last_error: message,
|
|
2181
|
+
output_tail: outputTail || null
|
|
2182
|
+
}).catch((reportErr) => {
|
|
2183
|
+
opts.log.warn(`workspace state report failed after setup error: ${String(reportErr)}`);
|
|
2184
|
+
});
|
|
2185
|
+
throw err;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function buildWorkspacePlan(input, defaultWorkspaceDir, serverConfigHash) {
|
|
2189
|
+
const config = normalizeConfig(input, defaultWorkspaceDir);
|
|
2190
|
+
const workspace = config.workspace;
|
|
2191
|
+
const { workspaceDir, customWorkspaceField } = resolveWorkspaceDir(workspace, defaultWorkspaceDir);
|
|
2192
|
+
const configHash = serverConfigHash || createHash("sha256").update(JSON.stringify(config)).digest("hex");
|
|
2193
|
+
return { config, workspace, workspaceDir, defaultWorkspaceDir, customWorkspaceField, configHash };
|
|
2194
|
+
}
|
|
2195
|
+
function normalizeConfig(input, defaultWorkspaceDir) {
|
|
2196
|
+
const cfg = input ? JSON.parse(JSON.stringify(input)) : {};
|
|
2197
|
+
cfg.workspace_path = cfg.workspace_path?.trim() || void 0;
|
|
2198
|
+
if (!cfg.workspace && cfg.workspace_path) {
|
|
2199
|
+
cfg.workspace = { mode: "local_path", path: cfg.workspace_path };
|
|
2200
|
+
}
|
|
2201
|
+
if (!cfg.workspace) {
|
|
2202
|
+
cfg.workspace = { mode: "default" };
|
|
2203
|
+
}
|
|
2204
|
+
const ws = cfg.workspace;
|
|
2205
|
+
ws.mode = ws.mode?.trim() || void 0;
|
|
2206
|
+
ws.path = ws.path?.trim() || void 0;
|
|
2207
|
+
if (ws.git) {
|
|
2208
|
+
ws.git.remote = ws.git.remote?.trim() || void 0;
|
|
2209
|
+
ws.git.ref = ws.git.ref?.trim() || void 0;
|
|
2210
|
+
ws.git.target_path = ws.git.target_path?.trim() || void 0;
|
|
2211
|
+
}
|
|
2212
|
+
ws.mode ||= ws.git ? "git" : ws.path ? "local_path" : "default";
|
|
2213
|
+
if (ws.mode === "local_path" && !ws.path) {
|
|
2214
|
+
ws.mode = "default";
|
|
2215
|
+
}
|
|
2216
|
+
if (ws.mode === "git") {
|
|
2217
|
+
ws.git ||= {};
|
|
2218
|
+
if (!ws.git.target_path && ws.path) {
|
|
2219
|
+
ws.git.target_path = ws.path;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (ws.mode === "default") {
|
|
2223
|
+
ws.path = void 0;
|
|
2224
|
+
ws.git = void 0;
|
|
2225
|
+
}
|
|
2226
|
+
if (ws.mode === "local_path") {
|
|
2227
|
+
ws.git = void 0;
|
|
2228
|
+
}
|
|
2229
|
+
if (ws.setup) {
|
|
2230
|
+
ws.setup.command = ws.setup.command?.trim();
|
|
2231
|
+
ws.setup.run_on ||= "first_attach";
|
|
2232
|
+
if (!ws.setup.command) {
|
|
2233
|
+
ws.setup = void 0;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
void defaultWorkspaceDir;
|
|
2237
|
+
return cfg;
|
|
2238
|
+
}
|
|
2239
|
+
function resolveWorkspaceDir(workspace, defaultWorkspaceDir) {
|
|
2240
|
+
if (workspace.mode === "local_path") {
|
|
2241
|
+
if (!workspace.path)
|
|
2242
|
+
return { workspaceDir: defaultWorkspaceDir };
|
|
2243
|
+
return {
|
|
2244
|
+
workspaceDir: requireAbsolute(workspace.path, "workspace.path"),
|
|
2245
|
+
customWorkspaceField: "workspace.path"
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
if (workspace.mode === "git") {
|
|
2249
|
+
return workspace.git?.target_path ? {
|
|
2250
|
+
workspaceDir: requireAbsolute(workspace.git.target_path, "workspace.git.target_path"),
|
|
2251
|
+
customWorkspaceField: "workspace.git.target_path"
|
|
2252
|
+
} : { workspaceDir: defaultWorkspaceDir };
|
|
2253
|
+
}
|
|
2254
|
+
return { workspaceDir: defaultWorkspaceDir };
|
|
2255
|
+
}
|
|
2256
|
+
async function ensureWorkspace(plan, log2) {
|
|
2257
|
+
const ws = plan.workspace;
|
|
2258
|
+
if (ws.mode === "default") {
|
|
2259
|
+
fs2.mkdirSync(plan.workspaceDir, { recursive: true });
|
|
2260
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (ws.mode === "local_path") {
|
|
2264
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2265
|
+
let st;
|
|
2266
|
+
try {
|
|
2267
|
+
st = fs2.statSync(plan.workspaceDir);
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
2270
|
+
throw new Error(`workspace path does not exist: ${plan.workspaceDir}`);
|
|
2271
|
+
}
|
|
2272
|
+
throw err;
|
|
2273
|
+
}
|
|
2274
|
+
if (!st.isDirectory()) {
|
|
2275
|
+
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2276
|
+
}
|
|
2277
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2278
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (ws.mode === "git") {
|
|
2282
|
+
const remote = ws.git?.remote?.trim();
|
|
2283
|
+
if (!remote)
|
|
2284
|
+
throw new Error("workspace git remote is required");
|
|
2285
|
+
if (plan.customWorkspaceField) {
|
|
2286
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2287
|
+
}
|
|
2288
|
+
if (!fs2.existsSync(plan.workspaceDir)) {
|
|
2289
|
+
fs2.mkdirSync(path3.dirname(plan.workspaceDir), { recursive: true });
|
|
2290
|
+
assertWritableWorkspaceDir(path3.dirname(plan.workspaceDir));
|
|
2291
|
+
await runCommand("git", ["clone", remote, plan.workspaceDir], process.cwd());
|
|
2292
|
+
} else {
|
|
2293
|
+
const st = fs2.statSync(plan.workspaceDir);
|
|
2294
|
+
if (!st.isDirectory()) {
|
|
2295
|
+
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2296
|
+
}
|
|
2297
|
+
if (plan.customWorkspaceField) {
|
|
2298
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2299
|
+
}
|
|
2300
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2301
|
+
await ensureGitWorktree(plan.workspaceDir);
|
|
2302
|
+
await ensureGitRemote(plan.workspaceDir, remote);
|
|
2303
|
+
}
|
|
2304
|
+
if (ws.git?.ref) {
|
|
2305
|
+
const dirty = await commandOutput("git", ["status", "--porcelain"], plan.workspaceDir);
|
|
2306
|
+
if (dirty.trim()) {
|
|
2307
|
+
throw new Error(`workspace git tree has local changes; refusing checkout: ${plan.workspaceDir}`);
|
|
2308
|
+
}
|
|
2309
|
+
await runCommand("git", ["fetch", "origin", "--prune"], plan.workspaceDir);
|
|
2310
|
+
await checkoutGitRef(plan.workspaceDir, ws.git.ref);
|
|
2311
|
+
}
|
|
2312
|
+
log2.info(`workspace ready: git ${remote} -> ${plan.workspaceDir}`);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
throw new Error(`unsupported workspace mode: ${ws.mode ?? "(empty)"}`);
|
|
2316
|
+
}
|
|
2317
|
+
async function verifyExistingWorkspace(plan, log2) {
|
|
2318
|
+
try {
|
|
2319
|
+
if (plan.customWorkspaceField) {
|
|
2320
|
+
assertSafeCustomWorkspacePath(plan);
|
|
2321
|
+
}
|
|
2322
|
+
const st = fs2.statSync(plan.workspaceDir);
|
|
2323
|
+
if (!st.isDirectory()) {
|
|
2324
|
+
log2.warn(`workspace ready state ignored: path is not a directory: ${plan.workspaceDir}`);
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
if (plan.customWorkspaceField) {
|
|
2328
|
+
assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
|
|
2329
|
+
}
|
|
2330
|
+
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2331
|
+
if (plan.workspace.mode === "git") {
|
|
2332
|
+
await ensureGitWorktree(plan.workspaceDir);
|
|
2333
|
+
const remote = plan.workspace.git?.remote?.trim();
|
|
2334
|
+
if (remote) {
|
|
2335
|
+
await ensureGitRemote(plan.workspaceDir, remote);
|
|
2336
|
+
}
|
|
2337
|
+
if (plan.workspace.git?.ref) {
|
|
2338
|
+
const head = (await commandOutput("git", ["rev-parse", "HEAD"], plan.workspaceDir)).trim();
|
|
2339
|
+
const expected = await resolveConfiguredGitRef(plan.workspaceDir, plan.workspace.git.ref);
|
|
2340
|
+
if (head !== expected) {
|
|
2341
|
+
log2.warn(`workspace ready state ignored: git HEAD ${head} does not match ${plan.workspace.git.ref} (${expected})`);
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return true;
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
log2.warn(`workspace ready state ignored: ${String(err)}`);
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
async function ensureGitWorktree(cwd) {
|
|
2353
|
+
const result = await commandOutput("git", ["rev-parse", "--is-inside-work-tree"], cwd);
|
|
2354
|
+
if (result.trim() !== "true") {
|
|
2355
|
+
throw new Error(`workspace path exists but is not a git worktree: ${cwd}`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async function ensureGitRemote(cwd, expectedRemote) {
|
|
2359
|
+
const actualRemote = (await commandOutput("git", ["remote", "get-url", "origin"], cwd)).trim();
|
|
2360
|
+
if (actualRemote !== expectedRemote) {
|
|
2361
|
+
throw new Error(`workspace git remote mismatch: expected ${expectedRemote}, got ${actualRemote}`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
async function checkoutGitRef(cwd, ref) {
|
|
2365
|
+
const remoteCommit = await resolveRemoteGitRef(cwd, ref);
|
|
2366
|
+
if (remoteCommit) {
|
|
2367
|
+
await runCommand("git", ["checkout", "-B", ref, remoteCommit], cwd);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
const targetCommit = await resolveGitCommit(cwd, ref);
|
|
2371
|
+
await runCommand("git", ["checkout", "--detach", targetCommit], cwd);
|
|
2372
|
+
}
|
|
2373
|
+
async function resolveConfiguredGitRef(cwd, ref) {
|
|
2374
|
+
const remote = await resolveRemoteGitRef(cwd, ref);
|
|
2375
|
+
return remote || await resolveGitCommit(cwd, ref);
|
|
2376
|
+
}
|
|
2377
|
+
async function resolveRemoteGitRef(cwd, ref) {
|
|
2378
|
+
return (await tryGitOutput("git", ["rev-parse", "--verify", `refs/remotes/origin/${ref}^{commit}`], cwd)).trim();
|
|
2379
|
+
}
|
|
2380
|
+
async function resolveGitCommit(cwd, ref) {
|
|
2381
|
+
const candidates = [`refs/tags/${ref}^{commit}`, `${ref}^{commit}`];
|
|
2382
|
+
for (const candidate of candidates) {
|
|
2383
|
+
const commit = await tryGitOutput("git", ["rev-parse", "--verify", candidate], cwd);
|
|
2384
|
+
if (commit.trim()) {
|
|
2385
|
+
return commit.trim();
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
throw new Error(`workspace git ref not found: ${ref}`);
|
|
2389
|
+
}
|
|
2390
|
+
function shouldRunSetup(setup, prior, configHash, forceSetup = false) {
|
|
2391
|
+
if (!setup?.command)
|
|
2392
|
+
return false;
|
|
2393
|
+
if (forceSetup)
|
|
2394
|
+
return true;
|
|
2395
|
+
if (setup.run_on === "config_change") {
|
|
2396
|
+
return prior?.config_hash !== configHash || prior.status === "pending" || prior.status === "failed";
|
|
2397
|
+
}
|
|
2398
|
+
return prior?.status !== "ready" || !prior.prepared_at;
|
|
2399
|
+
}
|
|
2400
|
+
async function runSetup(cwd, command, timeoutSec) {
|
|
2401
|
+
const effectiveTimeoutSec = timeoutSec && timeoutSec > 0 ? timeoutSec : DEFAULT_SETUP_TIMEOUT_SEC;
|
|
2402
|
+
return runCommand(process.env.SHELL || "/bin/sh", ["-lc", command], cwd, effectiveTimeoutSec * 1e3, scrubSetupEnv());
|
|
2403
|
+
}
|
|
2404
|
+
function scrubSetupEnv() {
|
|
2405
|
+
const env = { ...process.env };
|
|
2406
|
+
clearAllProviderCreds(env);
|
|
2407
|
+
delete env.PRLL_API_KEY;
|
|
2408
|
+
return env;
|
|
2409
|
+
}
|
|
2410
|
+
async function commandOutput(cmd, args, cwd) {
|
|
2411
|
+
return runCommand(cmd, args, cwd);
|
|
2412
|
+
}
|
|
2413
|
+
async function tryGitOutput(cmd, args, cwd) {
|
|
2414
|
+
try {
|
|
2415
|
+
return await runCommand(cmd, args, cwd);
|
|
2416
|
+
} catch {
|
|
2417
|
+
return "";
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
|
|
2421
|
+
return new Promise((resolve4, reject) => {
|
|
2422
|
+
let tail = "";
|
|
2423
|
+
let timedOut = false;
|
|
2424
|
+
let settled = false;
|
|
2425
|
+
let timeoutTimer = null;
|
|
2426
|
+
let killTimer = null;
|
|
2427
|
+
const append = (chunk) => {
|
|
2428
|
+
tail += chunk.toString();
|
|
2429
|
+
if (tail.length > OUTPUT_TAIL_LIMIT) {
|
|
2430
|
+
tail = tail.slice(-OUTPUT_TAIL_LIMIT);
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
const settle = (fn) => {
|
|
2434
|
+
if (settled)
|
|
2435
|
+
return;
|
|
2436
|
+
settled = true;
|
|
2437
|
+
if (timeoutTimer)
|
|
2438
|
+
clearTimeout(timeoutTimer);
|
|
2439
|
+
if (killTimer)
|
|
2440
|
+
clearTimeout(killTimer);
|
|
2441
|
+
fn();
|
|
2442
|
+
};
|
|
2443
|
+
const child = spawn(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2444
|
+
timeoutTimer = setTimeout(() => {
|
|
2445
|
+
timedOut = true;
|
|
2446
|
+
child.kill("SIGTERM");
|
|
2447
|
+
killTimer = setTimeout(() => {
|
|
2448
|
+
child.kill("SIGKILL");
|
|
2449
|
+
}, 5e3);
|
|
2450
|
+
}, timeoutMs);
|
|
2451
|
+
child.stdout?.on("data", append);
|
|
2452
|
+
child.stderr?.on("data", append);
|
|
2453
|
+
child.once("error", (err) => {
|
|
2454
|
+
settle(() => reject(err));
|
|
2455
|
+
});
|
|
2456
|
+
child.once("close", (code, signal) => {
|
|
2457
|
+
if (timedOut) {
|
|
2458
|
+
settle(() => reject(new Error(`command timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}
|
|
2459
|
+
${tail}`)));
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
if (code === 0) {
|
|
2463
|
+
settle(() => resolve4(tail));
|
|
2464
|
+
} else {
|
|
2465
|
+
settle(() => reject(new Error(`command failed (${code ?? signal}): ${cmd} ${args.join(" ")}
|
|
2466
|
+
${tail}`)));
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
function requireAbsolute(value, field) {
|
|
2472
|
+
if (!value || !path3.isAbsolute(value)) {
|
|
2473
|
+
throw new Error(`${field} must be an absolute path`);
|
|
2474
|
+
}
|
|
2475
|
+
return path3.resolve(value);
|
|
2476
|
+
}
|
|
2477
|
+
function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
|
|
2478
|
+
if (!plan.customWorkspaceField)
|
|
2479
|
+
return;
|
|
2480
|
+
const normalized = requireAbsolute(candidate, plan.customWorkspaceField);
|
|
2481
|
+
const reason = workspacePathDenyReason(toPolicyPath(normalized));
|
|
2482
|
+
if (reason) {
|
|
2483
|
+
throw new Error(`${plan.customWorkspaceField} must point to a project folder, not ${reason}: ${normalized}`);
|
|
2484
|
+
}
|
|
2485
|
+
const defaultWorkspace = path3.resolve(plan.defaultWorkspaceDir);
|
|
2486
|
+
if (isAncestorPath(normalized, defaultWorkspace) || normalized === defaultWorkspace) {
|
|
2487
|
+
throw new Error(`${plan.customWorkspaceField} must not point at daemon state directories: ${normalized}`);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
function assertWritableWorkspaceDir(dir) {
|
|
2491
|
+
fs2.accessSync(dir, fs2.constants.R_OK | fs2.constants.W_OK | fs2.constants.X_OK);
|
|
2492
|
+
const probe = path3.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
|
|
2493
|
+
const fd = fs2.openSync(probe, "wx", 384);
|
|
2494
|
+
fs2.closeSync(fd);
|
|
2495
|
+
fs2.unlinkSync(probe);
|
|
2496
|
+
}
|
|
2497
|
+
function workspacePathDenyReason(value) {
|
|
2498
|
+
if (value === "/")
|
|
2499
|
+
return "the filesystem root";
|
|
2500
|
+
if (["/tmp", "/private/tmp", "/var/tmp", "/Users", "/home"].includes(value)) {
|
|
2501
|
+
return "a shared or home root directory";
|
|
2502
|
+
}
|
|
2503
|
+
for (const root of [
|
|
2504
|
+
"/Applications",
|
|
2505
|
+
"/bin",
|
|
2506
|
+
"/boot",
|
|
2507
|
+
"/dev",
|
|
2508
|
+
"/etc",
|
|
2509
|
+
"/Library",
|
|
2510
|
+
"/private/etc",
|
|
2511
|
+
"/private/var/db",
|
|
2512
|
+
"/proc",
|
|
2513
|
+
"/root",
|
|
2514
|
+
"/run",
|
|
2515
|
+
"/sbin",
|
|
2516
|
+
"/System",
|
|
2517
|
+
"/sys",
|
|
2518
|
+
"/usr",
|
|
2519
|
+
"/var/db",
|
|
2520
|
+
"/var/root"
|
|
2521
|
+
]) {
|
|
2522
|
+
if (value === root || value.startsWith(`${root}/`)) {
|
|
2523
|
+
return "a system directory";
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
const parts = value.split("/").filter(Boolean);
|
|
2527
|
+
if (parts.includes(".git"))
|
|
2528
|
+
return "a git metadata directory";
|
|
2529
|
+
if (parts.length === 2 && (parts[0] === "Users" || parts[0] === "home")) {
|
|
2530
|
+
return "a home directory";
|
|
2531
|
+
}
|
|
2532
|
+
if (parts.length >= 3 && (parts[0] === "Users" || parts[0] === "home")) {
|
|
2533
|
+
const homeChild = parts[2];
|
|
2534
|
+
if ([
|
|
2535
|
+
".aws",
|
|
2536
|
+
".azure",
|
|
2537
|
+
".claude",
|
|
2538
|
+
".codex",
|
|
2539
|
+
".config",
|
|
2540
|
+
".docker",
|
|
2541
|
+
".gnupg",
|
|
2542
|
+
".kube",
|
|
2543
|
+
".local",
|
|
2544
|
+
".npm",
|
|
2545
|
+
".ssh",
|
|
2546
|
+
".parall-agent",
|
|
2547
|
+
".parall-daemon",
|
|
2548
|
+
"Library"
|
|
2549
|
+
].includes(homeChild)) {
|
|
2550
|
+
return "a credential or application state directory";
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return "";
|
|
2554
|
+
}
|
|
2555
|
+
function isAncestorPath(parent, child) {
|
|
2556
|
+
const relative2 = path3.relative(parent, child);
|
|
2557
|
+
return relative2 !== "" && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
|
|
2558
|
+
}
|
|
2559
|
+
function toPolicyPath(value) {
|
|
2560
|
+
return path3.resolve(value).split(path3.sep).join("/");
|
|
2561
|
+
}
|
|
2562
|
+
function isNodeError(err) {
|
|
2563
|
+
return err instanceof Error && "code" in err;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2061
2566
|
// ts/daemon/dist/supervisor.js
|
|
2062
2567
|
var RUNTIME_PACKAGES = {
|
|
2063
2568
|
"claude-code": "@parall/claude-agent",
|
|
2064
2569
|
"codex": "@parall/codex-agent",
|
|
2065
2570
|
"openclaw": "@parall/openclaw-agent"
|
|
2066
2571
|
};
|
|
2572
|
+
var WORKSPACE_SETUP_RETRY_DELAY_MS = 5e3;
|
|
2067
2573
|
function sleepCancellable(ms, signal) {
|
|
2068
2574
|
if (signal.aborted)
|
|
2069
2575
|
return Promise.resolve(false);
|
|
2070
|
-
return new Promise((
|
|
2576
|
+
return new Promise((resolve4) => {
|
|
2071
2577
|
const timer = setTimeout(() => {
|
|
2072
2578
|
signal.removeEventListener("abort", onAbort);
|
|
2073
|
-
|
|
2579
|
+
resolve4(true);
|
|
2074
2580
|
}, ms);
|
|
2075
2581
|
const onAbort = () => {
|
|
2076
2582
|
clearTimeout(timer);
|
|
2077
|
-
|
|
2583
|
+
resolve4(false);
|
|
2078
2584
|
};
|
|
2079
2585
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2080
2586
|
});
|
|
@@ -2084,6 +2590,10 @@ var DaemonSupervisor = class {
|
|
|
2084
2590
|
client;
|
|
2085
2591
|
log;
|
|
2086
2592
|
children = /* @__PURE__ */ new Map();
|
|
2593
|
+
spawningAgents = /* @__PURE__ */ new Set();
|
|
2594
|
+
pendingWorkspaceSetup = /* @__PURE__ */ new Set();
|
|
2595
|
+
workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
|
|
2596
|
+
cancelledSpawns = /* @__PURE__ */ new Set();
|
|
2087
2597
|
ws = null;
|
|
2088
2598
|
running = false;
|
|
2089
2599
|
machineOrgId = null;
|
|
@@ -2144,6 +2654,10 @@ var DaemonSupervisor = class {
|
|
|
2144
2654
|
void this.respawnAllChildren();
|
|
2145
2655
|
}
|
|
2146
2656
|
});
|
|
2657
|
+
this.ws.on("machine.workspace.setup.requested", (data) => {
|
|
2658
|
+
this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
|
|
2659
|
+
void this.handleWorkspaceSetupRequested(data.agent_id);
|
|
2660
|
+
});
|
|
2147
2661
|
this.ws.on("machine.stop", (data) => {
|
|
2148
2662
|
this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
|
|
2149
2663
|
void this.stop();
|
|
@@ -2154,8 +2668,8 @@ var DaemonSupervisor = class {
|
|
|
2154
2668
|
}
|
|
2155
2669
|
});
|
|
2156
2670
|
await this.ws.connect();
|
|
2157
|
-
await new Promise((
|
|
2158
|
-
this.stopResolve =
|
|
2671
|
+
await new Promise((resolve4) => {
|
|
2672
|
+
this.stopResolve = resolve4;
|
|
2159
2673
|
});
|
|
2160
2674
|
signal.removeEventListener("abort", onAbort);
|
|
2161
2675
|
}
|
|
@@ -2169,6 +2683,10 @@ var DaemonSupervisor = class {
|
|
|
2169
2683
|
this.ws = null;
|
|
2170
2684
|
}
|
|
2171
2685
|
const exits = [];
|
|
2686
|
+
for (const timer of this.workspaceSetupRetryTimers.values()) {
|
|
2687
|
+
clearTimeout(timer);
|
|
2688
|
+
}
|
|
2689
|
+
this.workspaceSetupRetryTimers.clear();
|
|
2172
2690
|
for (const state of this.children.values()) {
|
|
2173
2691
|
state.shuttingDown = true;
|
|
2174
2692
|
if (state.restartTimer) {
|
|
@@ -2270,15 +2788,15 @@ var DaemonSupervisor = class {
|
|
|
2270
2788
|
*/
|
|
2271
2789
|
migrateFlatLayout() {
|
|
2272
2790
|
const root = this.config.rootStateDir;
|
|
2273
|
-
const agentsDir =
|
|
2274
|
-
const flatWorkspace =
|
|
2275
|
-
if (!
|
|
2791
|
+
const agentsDir = path4.join(root, "agents");
|
|
2792
|
+
const flatWorkspace = path4.join(root, "workspace");
|
|
2793
|
+
if (!fs3.existsSync(flatWorkspace) || fs3.existsSync(agentsDir))
|
|
2276
2794
|
return;
|
|
2277
2795
|
let ownerAgentId;
|
|
2278
|
-
const sessionsDir =
|
|
2279
|
-
if (
|
|
2796
|
+
const sessionsDir = path4.join(root, "sessions");
|
|
2797
|
+
if (fs3.existsSync(sessionsDir)) {
|
|
2280
2798
|
try {
|
|
2281
|
-
for (const file of
|
|
2799
|
+
for (const file of fs3.readdirSync(sessionsDir)) {
|
|
2282
2800
|
if (!file.endsWith(".json"))
|
|
2283
2801
|
continue;
|
|
2284
2802
|
const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
|
|
@@ -2292,13 +2810,13 @@ var DaemonSupervisor = class {
|
|
|
2292
2810
|
}
|
|
2293
2811
|
}
|
|
2294
2812
|
const targetId = ownerAgentId ?? "_orphan";
|
|
2295
|
-
const targetDir =
|
|
2813
|
+
const targetDir = path4.join(agentsDir, targetId);
|
|
2296
2814
|
try {
|
|
2297
|
-
|
|
2815
|
+
fs3.mkdirSync(targetDir, { recursive: true });
|
|
2298
2816
|
for (const sub of ["workspace", "sessions", "dispatch-context"]) {
|
|
2299
|
-
const src =
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2817
|
+
const src = path4.join(root, sub);
|
|
2818
|
+
if (fs3.existsSync(src)) {
|
|
2819
|
+
fs3.renameSync(src, path4.join(targetDir, sub));
|
|
2302
2820
|
}
|
|
2303
2821
|
}
|
|
2304
2822
|
this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
|
|
@@ -2307,23 +2825,30 @@ var DaemonSupervisor = class {
|
|
|
2307
2825
|
}
|
|
2308
2826
|
}
|
|
2309
2827
|
// ---- WS event handlers (incremental) ----
|
|
2310
|
-
async
|
|
2311
|
-
if (this.children.has(agentId))
|
|
2312
|
-
return;
|
|
2828
|
+
async fetchAttachedAgent(agentId) {
|
|
2313
2829
|
let attached;
|
|
2314
2830
|
try {
|
|
2315
2831
|
attached = await this.client.listAttachedAgents();
|
|
2316
2832
|
} catch (err) {
|
|
2317
|
-
this.log.warn(`
|
|
2318
|
-
return;
|
|
2833
|
+
this.log.warn(`fetchAttachedAgent: listAttachedAgents failed: ${String(err)}`);
|
|
2834
|
+
return { kind: "retryable" };
|
|
2319
2835
|
}
|
|
2320
2836
|
const entry = attached.find((a) => (a.user?.id ?? a.profile.user_id) === agentId);
|
|
2321
2837
|
if (!entry) {
|
|
2322
|
-
this.log.warn(`
|
|
2323
|
-
return;
|
|
2838
|
+
this.log.warn(`fetchAttachedAgent: agent ${agentId} not found in attached list`);
|
|
2839
|
+
return { kind: "skip" };
|
|
2324
2840
|
}
|
|
2325
2841
|
if (entry.user && entry.user.status !== "active") {
|
|
2326
2842
|
this.log.info(`agent ${agentId} not active (status=${entry.user.status}) \u2014 skipping`);
|
|
2843
|
+
return { kind: "skip" };
|
|
2844
|
+
}
|
|
2845
|
+
return { kind: "found", entry };
|
|
2846
|
+
}
|
|
2847
|
+
async handleAgentAttached(agentId) {
|
|
2848
|
+
if (this.children.has(agentId) || this.spawningAgents.has(agentId))
|
|
2849
|
+
return;
|
|
2850
|
+
const result = await this.fetchAttachedAgent(agentId);
|
|
2851
|
+
if (result.kind !== "found") {
|
|
2327
2852
|
return;
|
|
2328
2853
|
}
|
|
2329
2854
|
const orgId = this.machineOrgId;
|
|
@@ -2331,9 +2856,14 @@ var DaemonSupervisor = class {
|
|
|
2331
2856
|
this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
|
|
2332
2857
|
return;
|
|
2333
2858
|
}
|
|
2334
|
-
await this.spawnAgent(agentId, orgId, entry);
|
|
2859
|
+
await this.spawnAgent(agentId, orgId, result.entry);
|
|
2335
2860
|
}
|
|
2336
2861
|
async handleAgentDetached(agentId) {
|
|
2862
|
+
this.pendingWorkspaceSetup.delete(agentId);
|
|
2863
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2864
|
+
if (this.spawningAgents.has(agentId)) {
|
|
2865
|
+
this.cancelledSpawns.add(agentId);
|
|
2866
|
+
}
|
|
2337
2867
|
const state = this.children.get(agentId);
|
|
2338
2868
|
if (!state)
|
|
2339
2869
|
return;
|
|
@@ -2346,6 +2876,61 @@ var DaemonSupervisor = class {
|
|
|
2346
2876
|
await this.terminateChild(state);
|
|
2347
2877
|
this.children.delete(agentId);
|
|
2348
2878
|
}
|
|
2879
|
+
async handleWorkspaceSetupRequested(agentId) {
|
|
2880
|
+
if (this.spawningAgents.has(agentId)) {
|
|
2881
|
+
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|
|
2882
|
+
this.pendingWorkspaceSetup.add(agentId);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const result = await this.fetchAttachedAgent(agentId);
|
|
2886
|
+
if (result.kind === "retryable") {
|
|
2887
|
+
this.pendingWorkspaceSetup.add(agentId);
|
|
2888
|
+
this.scheduleWorkspaceSetupRetry(agentId);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
if (result.kind === "skip") {
|
|
2892
|
+
this.pendingWorkspaceSetup.delete(agentId);
|
|
2893
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
this.clearWorkspaceSetupRetry(agentId);
|
|
2897
|
+
const state = this.children.get(agentId);
|
|
2898
|
+
if (state) {
|
|
2899
|
+
this.log.info(`agent ${agentId}: restarting for workspace setup`);
|
|
2900
|
+
state.shuttingDown = true;
|
|
2901
|
+
if (state.restartTimer) {
|
|
2902
|
+
clearTimeout(state.restartTimer);
|
|
2903
|
+
state.restartTimer = null;
|
|
2904
|
+
}
|
|
2905
|
+
await this.terminateChild(state);
|
|
2906
|
+
this.children.delete(agentId);
|
|
2907
|
+
}
|
|
2908
|
+
const orgId = this.machineOrgId;
|
|
2909
|
+
if (!orgId) {
|
|
2910
|
+
this.log.warn(`agent ${agentId}: no org_id available yet; skipping`);
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
await this.spawnAgent(agentId, orgId, result.entry);
|
|
2914
|
+
}
|
|
2915
|
+
scheduleWorkspaceSetupRetry(agentId) {
|
|
2916
|
+
if (!this.running || this.workspaceSetupRetryTimers.has(agentId))
|
|
2917
|
+
return;
|
|
2918
|
+
const timer = setTimeout(() => {
|
|
2919
|
+
this.workspaceSetupRetryTimers.delete(agentId);
|
|
2920
|
+
if (this.running && this.pendingWorkspaceSetup.has(agentId)) {
|
|
2921
|
+
void this.handleWorkspaceSetupRequested(agentId);
|
|
2922
|
+
}
|
|
2923
|
+
}, WORKSPACE_SETUP_RETRY_DELAY_MS);
|
|
2924
|
+
timer.unref?.();
|
|
2925
|
+
this.workspaceSetupRetryTimers.set(agentId, timer);
|
|
2926
|
+
}
|
|
2927
|
+
clearWorkspaceSetupRetry(agentId) {
|
|
2928
|
+
const timer = this.workspaceSetupRetryTimers.get(agentId);
|
|
2929
|
+
if (timer) {
|
|
2930
|
+
clearTimeout(timer);
|
|
2931
|
+
this.workspaceSetupRetryTimers.delete(agentId);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2349
2934
|
// ---- Spawn / restart ----
|
|
2350
2935
|
async refreshMachineConfig() {
|
|
2351
2936
|
try {
|
|
@@ -2385,6 +2970,25 @@ var DaemonSupervisor = class {
|
|
|
2385
2970
|
}
|
|
2386
2971
|
}
|
|
2387
2972
|
async spawnAgent(agentId, orgId, attached) {
|
|
2973
|
+
if (this.children.has(agentId) || this.spawningAgents.has(agentId)) {
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
this.spawningAgents.add(agentId);
|
|
2977
|
+
let shouldReplaySetupRequest = false;
|
|
2978
|
+
try {
|
|
2979
|
+
await this.spawnAgentOnce(agentId, orgId, attached);
|
|
2980
|
+
} finally {
|
|
2981
|
+
this.spawningAgents.delete(agentId);
|
|
2982
|
+
this.cancelledSpawns.delete(agentId);
|
|
2983
|
+
shouldReplaySetupRequest = this.pendingWorkspaceSetup.delete(agentId);
|
|
2984
|
+
}
|
|
2985
|
+
if (shouldReplaySetupRequest && this.running) {
|
|
2986
|
+
queueMicrotask(() => {
|
|
2987
|
+
void this.handleWorkspaceSetupRequested(agentId);
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
async spawnAgentOnce(agentId, orgId, attached) {
|
|
2388
2992
|
let credential;
|
|
2389
2993
|
try {
|
|
2390
2994
|
credential = await this.client.mintLaunchCredential(agentId);
|
|
@@ -2393,22 +2997,36 @@ var DaemonSupervisor = class {
|
|
|
2393
2997
|
return;
|
|
2394
2998
|
}
|
|
2395
2999
|
const stateDir = agentStateDirFor(this.config.rootStateDir, agentId);
|
|
2396
|
-
const
|
|
3000
|
+
const defaultWorkspaceDir = agentWorkspaceDirFor(this.config.rootStateDir, agentId);
|
|
2397
3001
|
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
2398
3002
|
const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
|
|
2399
3003
|
try {
|
|
2400
|
-
|
|
2401
|
-
if (!attached.daemon_config?.workspace_path) {
|
|
2402
|
-
fs2.mkdirSync(workspaceDir, { recursive: true });
|
|
2403
|
-
}
|
|
3004
|
+
fs3.mkdirSync(stateDir, { recursive: true });
|
|
2404
3005
|
if (isK8s) {
|
|
2405
|
-
|
|
3006
|
+
fs3.mkdirSync(claudeHome, { recursive: true });
|
|
2406
3007
|
this.ensureSharedCredentialLink(claudeHome, agentId);
|
|
2407
3008
|
}
|
|
2408
3009
|
} catch (err) {
|
|
2409
3010
|
this.log.warn(`mkdir agent dirs (${agentId}) failed: ${String(err)}`);
|
|
2410
3011
|
}
|
|
2411
3012
|
const runtimeType = attached.profile.runtime_type ?? "claude-code";
|
|
3013
|
+
let workspaceDir;
|
|
3014
|
+
try {
|
|
3015
|
+
workspaceDir = await prepareWorkspace({
|
|
3016
|
+
client: this.client,
|
|
3017
|
+
attached,
|
|
3018
|
+
agentId,
|
|
3019
|
+
defaultWorkspaceDir,
|
|
3020
|
+
log: this.log
|
|
3021
|
+
});
|
|
3022
|
+
} catch (err) {
|
|
3023
|
+
this.log.error(`agent ${agentId}: workspace setup failed: ${String(err)}`);
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
if (this.cancelledSpawns.delete(agentId)) {
|
|
3027
|
+
this.log.info(`agent ${agentId}: spawn cancelled after detach during workspace setup`);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
2412
3030
|
const state = {
|
|
2413
3031
|
agentId,
|
|
2414
3032
|
orgId,
|
|
@@ -2441,7 +3059,7 @@ var DaemonSupervisor = class {
|
|
|
2441
3059
|
};
|
|
2442
3060
|
const env = adapter.buildEnv({ ...process.env, PRLL_API_URL: this.config.apiUrl }, state.agentId, state.orgId, state.credential.api_key, dirs, state.providerConfig);
|
|
2443
3061
|
this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} bin=${adapter.bin} (attempt ${state.restartAttempts + 1})`);
|
|
2444
|
-
const child =
|
|
3062
|
+
const child = spawn2(adapter.bin, [], {
|
|
2445
3063
|
env,
|
|
2446
3064
|
stdio: ["ignore", "inherit", "inherit"],
|
|
2447
3065
|
detached: false
|
|
@@ -2479,25 +3097,26 @@ var DaemonSupervisor = class {
|
|
|
2479
3097
|
settleChild("error", null, null, err);
|
|
2480
3098
|
});
|
|
2481
3099
|
child.once("close", (code, signal) => settleChild("close", code, signal));
|
|
2482
|
-
setTimeout(() => {
|
|
3100
|
+
const stableTimer = setTimeout(() => {
|
|
2483
3101
|
if (state.child === child) {
|
|
2484
3102
|
state.restartAttempts = 0;
|
|
2485
3103
|
}
|
|
2486
3104
|
}, Math.max(this.config.restartBackoffMs, 3e4));
|
|
3105
|
+
stableTimer.unref?.();
|
|
2487
3106
|
}
|
|
2488
3107
|
async terminateChild(state) {
|
|
2489
3108
|
const child = state.child;
|
|
2490
3109
|
if (!child)
|
|
2491
3110
|
return;
|
|
2492
|
-
return new Promise((
|
|
2493
|
-
const onExit = () =>
|
|
3111
|
+
return new Promise((resolve4) => {
|
|
3112
|
+
const onExit = () => resolve4();
|
|
2494
3113
|
child.once("exit", onExit);
|
|
2495
3114
|
try {
|
|
2496
3115
|
child.kill("SIGTERM");
|
|
2497
3116
|
} catch (err) {
|
|
2498
3117
|
this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
|
|
2499
3118
|
child.off("exit", onExit);
|
|
2500
|
-
|
|
3119
|
+
resolve4();
|
|
2501
3120
|
return;
|
|
2502
3121
|
}
|
|
2503
3122
|
const hardKill = setTimeout(() => {
|
|
@@ -2510,60 +3129,60 @@ var DaemonSupervisor = class {
|
|
|
2510
3129
|
});
|
|
2511
3130
|
}
|
|
2512
3131
|
ensureSharedCredentialLink(agentClaudeHome, agentId) {
|
|
2513
|
-
const sharedCredentials =
|
|
3132
|
+
const sharedCredentials = path4.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
|
|
2514
3133
|
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
2515
|
-
const agentCredentialsDir =
|
|
2516
|
-
|
|
2517
|
-
|
|
3134
|
+
const agentCredentialsDir = path4.dirname(agentCredentials);
|
|
3135
|
+
fs3.mkdirSync(path4.dirname(sharedCredentials), { recursive: true });
|
|
3136
|
+
fs3.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
2518
3137
|
try {
|
|
2519
|
-
const existing =
|
|
3138
|
+
const existing = fs3.lstatSync(agentCredentials);
|
|
2520
3139
|
if (existing.isSymbolicLink()) {
|
|
2521
|
-
const currentTarget =
|
|
2522
|
-
if (
|
|
3140
|
+
const currentTarget = fs3.readlinkSync(agentCredentials);
|
|
3141
|
+
if (path4.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
2523
3142
|
return;
|
|
2524
3143
|
}
|
|
2525
|
-
|
|
3144
|
+
fs3.unlinkSync(agentCredentials);
|
|
2526
3145
|
} else if (existing.isDirectory()) {
|
|
2527
3146
|
this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
2528
3147
|
return;
|
|
2529
3148
|
} else {
|
|
2530
|
-
|
|
3149
|
+
fs3.unlinkSync(agentCredentials);
|
|
2531
3150
|
}
|
|
2532
3151
|
} catch (err) {
|
|
2533
3152
|
if (err.code !== "ENOENT") {
|
|
2534
3153
|
throw err;
|
|
2535
3154
|
}
|
|
2536
3155
|
}
|
|
2537
|
-
|
|
3156
|
+
fs3.symlinkSync(sharedCredentials, agentCredentials);
|
|
2538
3157
|
}
|
|
2539
3158
|
};
|
|
2540
3159
|
|
|
2541
3160
|
// ts/daemon/dist/cli.js
|
|
2542
|
-
import * as
|
|
2543
|
-
import * as
|
|
3161
|
+
import * as fs4 from "node:fs";
|
|
3162
|
+
import * as path5 from "node:path";
|
|
2544
3163
|
import * as os2 from "node:os";
|
|
2545
3164
|
import * as readline from "node:readline";
|
|
2546
|
-
import { spawn as
|
|
3165
|
+
import { spawn as spawn3, execSync } from "node:child_process";
|
|
2547
3166
|
var CONFIG_DIR = daemonConfigDir();
|
|
2548
3167
|
var CONFIG_PATH = daemonConfigPath();
|
|
2549
3168
|
function readConfig() {
|
|
2550
3169
|
try {
|
|
2551
|
-
return JSON.parse(
|
|
3170
|
+
return JSON.parse(fs4.readFileSync(CONFIG_PATH, "utf-8"));
|
|
2552
3171
|
} catch {
|
|
2553
3172
|
return null;
|
|
2554
3173
|
}
|
|
2555
3174
|
}
|
|
2556
3175
|
function writeConfig(config) {
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
3176
|
+
fs4.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3177
|
+
fs4.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3178
|
+
fs4.chmodSync(CONFIG_PATH, 384);
|
|
2560
3179
|
}
|
|
2561
3180
|
function prompt(question) {
|
|
2562
3181
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2563
|
-
return new Promise((
|
|
3182
|
+
return new Promise((resolve4) => {
|
|
2564
3183
|
rl.question(question, (answer) => {
|
|
2565
3184
|
rl.close();
|
|
2566
|
-
|
|
3185
|
+
resolve4(answer.trim());
|
|
2567
3186
|
});
|
|
2568
3187
|
});
|
|
2569
3188
|
}
|
|
@@ -2575,10 +3194,10 @@ function isLinux() {
|
|
|
2575
3194
|
}
|
|
2576
3195
|
var PLIST_LABEL = "com.parall.daemon";
|
|
2577
3196
|
function plistPath() {
|
|
2578
|
-
return
|
|
3197
|
+
return path5.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
2579
3198
|
}
|
|
2580
3199
|
function systemdUnitPath() {
|
|
2581
|
-
return
|
|
3200
|
+
return path5.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
|
|
2582
3201
|
}
|
|
2583
3202
|
function getDaemonBin() {
|
|
2584
3203
|
try {
|
|
@@ -2588,7 +3207,7 @@ function getDaemonBin() {
|
|
|
2588
3207
|
}
|
|
2589
3208
|
}
|
|
2590
3209
|
function generatePlist(daemonBin) {
|
|
2591
|
-
const logPath =
|
|
3210
|
+
const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
2592
3211
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2593
3212
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2594
3213
|
<plist version="1.0">
|
|
@@ -2635,16 +3254,16 @@ function installService() {
|
|
|
2635
3254
|
}
|
|
2636
3255
|
const bin = getDaemonBin();
|
|
2637
3256
|
if (isMacOS()) {
|
|
2638
|
-
const dir =
|
|
2639
|
-
|
|
2640
|
-
|
|
3257
|
+
const dir = path5.dirname(plistPath());
|
|
3258
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3259
|
+
fs4.writeFileSync(plistPath(), generatePlist(bin));
|
|
2641
3260
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2642
3261
|
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
|
|
2643
3262
|
console.log(`launchd agent installed: ${plistPath()}`);
|
|
2644
3263
|
} else if (isLinux()) {
|
|
2645
|
-
const dir =
|
|
2646
|
-
|
|
2647
|
-
|
|
3264
|
+
const dir = path5.dirname(systemdUnitPath());
|
|
3265
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
3266
|
+
fs4.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
|
|
2648
3267
|
execSync("systemctl --user daemon-reload");
|
|
2649
3268
|
execSync("systemctl --user enable --now parall-daemon");
|
|
2650
3269
|
console.log(`systemd service installed: ${systemdUnitPath()}`);
|
|
@@ -2705,31 +3324,31 @@ function cmdStop() {
|
|
|
2705
3324
|
}
|
|
2706
3325
|
function cmdLogs(lines) {
|
|
2707
3326
|
if (isLinux()) {
|
|
2708
|
-
const child2 =
|
|
3327
|
+
const child2 = spawn3("journalctl", ["--user-unit", "parall-daemon", "-n", lines, "-f"], {
|
|
2709
3328
|
stdio: "inherit"
|
|
2710
3329
|
});
|
|
2711
3330
|
child2.on("exit", (code) => process.exit(code ?? 0));
|
|
2712
3331
|
return;
|
|
2713
3332
|
}
|
|
2714
|
-
const logPath =
|
|
2715
|
-
if (!
|
|
3333
|
+
const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
3334
|
+
if (!fs4.existsSync(logPath)) {
|
|
2716
3335
|
console.log("No log file found at", logPath);
|
|
2717
3336
|
return;
|
|
2718
3337
|
}
|
|
2719
|
-
const child =
|
|
3338
|
+
const child = spawn3("tail", ["-n", lines, "-f", logPath], { stdio: "inherit" });
|
|
2720
3339
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
2721
3340
|
}
|
|
2722
3341
|
function cmdServiceUninstall() {
|
|
2723
3342
|
if (isMacOS()) {
|
|
2724
3343
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
2725
|
-
if (
|
|
2726
|
-
|
|
3344
|
+
if (fs4.existsSync(plistPath()))
|
|
3345
|
+
fs4.unlinkSync(plistPath());
|
|
2727
3346
|
console.log("launchd agent uninstalled.");
|
|
2728
3347
|
} else if (isLinux()) {
|
|
2729
3348
|
execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
|
|
2730
3349
|
execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
|
|
2731
|
-
if (
|
|
2732
|
-
|
|
3350
|
+
if (fs4.existsSync(systemdUnitPath()))
|
|
3351
|
+
fs4.unlinkSync(systemdUnitPath());
|
|
2733
3352
|
execSync("systemctl --user daemon-reload");
|
|
2734
3353
|
console.log("systemd service uninstalled.");
|
|
2735
3354
|
} else {
|
|
@@ -2846,7 +3465,8 @@ async function main() {
|
|
|
2846
3465
|
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
2847
3466
|
const client = new ParallClient({
|
|
2848
3467
|
baseUrl: config.apiUrl,
|
|
2849
|
-
token: config.apiKey
|
|
3468
|
+
token: config.apiKey,
|
|
3469
|
+
swimlaneName: config.swimlaneName
|
|
2850
3470
|
});
|
|
2851
3471
|
const abortController = new AbortController();
|
|
2852
3472
|
const onSignal = (sig) => {
|
|
@@ -2863,7 +3483,7 @@ async function main() {
|
|
|
2863
3483
|
process.exitCode = 1;
|
|
2864
3484
|
process.exit(1);
|
|
2865
3485
|
});
|
|
2866
|
-
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl);
|
|
3486
|
+
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
2867
3487
|
await runForever(config, client, log, abortController.signal);
|
|
2868
3488
|
}
|
|
2869
3489
|
var cliArgs = process.argv.slice(2);
|