ragent-cli 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +646 -231
- package/dist/sbom.json +1370 -2
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "ragent-cli",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.11.0",
|
|
35
35
|
description: "CLI agent for rAgent Live \u2014 browser-first terminal control plane for AI coding agents",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
@@ -86,6 +86,7 @@ var require_package = __commonJS({
|
|
|
86
86
|
},
|
|
87
87
|
dependencies: {
|
|
88
88
|
"@azure/web-pubsub-client": "^1.0.2",
|
|
89
|
+
"@ragent/transcript-protocol": "*",
|
|
89
90
|
commander: "^14.0.3",
|
|
90
91
|
figlet: "^1.9.3",
|
|
91
92
|
"node-pty": "^1.1.0",
|
|
@@ -97,7 +98,7 @@ var require_package = __commonJS({
|
|
|
97
98
|
"@cyclonedx/cyclonedx-npm": "^4.2.1",
|
|
98
99
|
"@eslint/js": "^10.0.1",
|
|
99
100
|
"@types/figlet": "^1.7.0",
|
|
100
|
-
"@types/node": "^
|
|
101
|
+
"@types/node": "^25.5.0",
|
|
101
102
|
"@types/ws": "^8.5.13",
|
|
102
103
|
eslint: "^10.0.2",
|
|
103
104
|
tsup: "^8.4.0",
|
|
@@ -135,10 +136,51 @@ var DEFAULT_RECONNECT_DELAY_MS = 2500;
|
|
|
135
136
|
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
136
137
|
var OUTPUT_BUFFER_MAX_BYTES = 100 * 1024;
|
|
137
138
|
|
|
139
|
+
// src/log.ts
|
|
140
|
+
var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
141
|
+
var configuredLevel = process.env.RAGENT_LOG_LEVEL && LEVELS[process.env.RAGENT_LOG_LEVEL] !== void 0 ? process.env.RAGENT_LOG_LEVEL : "info";
|
|
142
|
+
var threshold = LEVELS[configuredLevel];
|
|
143
|
+
function fmt(module2, message, fields) {
|
|
144
|
+
if (!fields || Object.keys(fields).length === 0) {
|
|
145
|
+
return `[${module2}] ${message}`;
|
|
146
|
+
}
|
|
147
|
+
const pairs = Object.entries(fields).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ");
|
|
148
|
+
return `[${module2}] ${message} ${pairs}`;
|
|
149
|
+
}
|
|
150
|
+
function formatValue(v) {
|
|
151
|
+
if (v === null) return "null";
|
|
152
|
+
if (v === void 0) return "undefined";
|
|
153
|
+
if (typeof v === "string") return v.includes(" ") ? JSON.stringify(v) : v;
|
|
154
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
155
|
+
if (v instanceof Error) return JSON.stringify(v.message);
|
|
156
|
+
try {
|
|
157
|
+
return JSON.stringify(v);
|
|
158
|
+
} catch {
|
|
159
|
+
return "[unserializable]";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function createLogger(module2) {
|
|
163
|
+
return {
|
|
164
|
+
debug(message, fields) {
|
|
165
|
+
if (threshold <= LEVELS.debug) console.debug(fmt(module2, message, fields));
|
|
166
|
+
},
|
|
167
|
+
info(message, fields) {
|
|
168
|
+
if (threshold <= LEVELS.info) console.log(fmt(module2, message, fields));
|
|
169
|
+
},
|
|
170
|
+
warn(message, fields) {
|
|
171
|
+
if (threshold <= LEVELS.warn) console.warn(fmt(module2, message, fields));
|
|
172
|
+
},
|
|
173
|
+
error(message, fields) {
|
|
174
|
+
if (threshold <= LEVELS.error) console.error(fmt(module2, message, fields));
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
138
179
|
// src/config.ts
|
|
139
180
|
var crypto = __toESM(require("crypto"));
|
|
140
181
|
var fs = __toESM(require("fs"));
|
|
141
182
|
var os2 = __toESM(require("os"));
|
|
183
|
+
var log = createLogger("config");
|
|
142
184
|
function ensureConfigDir() {
|
|
143
185
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
144
186
|
}
|
|
@@ -147,9 +189,10 @@ function loadConfig() {
|
|
|
147
189
|
const stat = fs.statSync(CONFIG_FILE);
|
|
148
190
|
const mode = stat.mode & 511;
|
|
149
191
|
if (mode & 63) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
192
|
+
log.warn("config file has overly permissive permissions; fixing to 0600", {
|
|
193
|
+
path: CONFIG_FILE,
|
|
194
|
+
mode: mode.toString(8)
|
|
195
|
+
});
|
|
153
196
|
fs.chmodSync(CONFIG_FILE, 384);
|
|
154
197
|
}
|
|
155
198
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
@@ -204,6 +247,7 @@ function inferHostId() {
|
|
|
204
247
|
}
|
|
205
248
|
|
|
206
249
|
// src/version.ts
|
|
250
|
+
var log2 = createLogger("version");
|
|
207
251
|
function parseSemver(version) {
|
|
208
252
|
const match = String(version || "").trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
209
253
|
if (!match) return null;
|
|
@@ -260,9 +304,10 @@ async function maybeWarnUpdate() {
|
|
|
260
304
|
if (process.env.RAGENT_DISABLE_UPDATE_CHECK === "true") return;
|
|
261
305
|
const latestVersion = await checkForUpdate({ force: false });
|
|
262
306
|
if (!latestVersion) return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
307
|
+
log2.info("update available; run: ragent update", {
|
|
308
|
+
current: CURRENT_VERSION,
|
|
309
|
+
latest: latestVersion
|
|
310
|
+
});
|
|
266
311
|
}
|
|
267
312
|
|
|
268
313
|
// src/commands/connect.ts
|
|
@@ -284,6 +329,7 @@ var os4 = __toESM(require("os"));
|
|
|
284
329
|
// src/system.ts
|
|
285
330
|
var import_node_child_process = require("child_process");
|
|
286
331
|
var readline = __toESM(require("readline/promises"));
|
|
332
|
+
var log3 = createLogger("system");
|
|
287
333
|
function wait(ms) {
|
|
288
334
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
289
335
|
}
|
|
@@ -349,8 +395,8 @@ async function askYesNo(question, defaultYes = true) {
|
|
|
349
395
|
async function installTmuxInteractively() {
|
|
350
396
|
const recipe = await chooseTmuxInstallCommand();
|
|
351
397
|
if (!recipe) {
|
|
352
|
-
|
|
353
|
-
|
|
398
|
+
log3.info("no supported package manager was detected for tmux auto-install");
|
|
399
|
+
log3.info("install tmux manually, then rerun: ragent doctor --fix");
|
|
354
400
|
return false;
|
|
355
401
|
}
|
|
356
402
|
const approved = await askYesNo(
|
|
@@ -376,6 +422,14 @@ var os3 = __toESM(require("os"));
|
|
|
376
422
|
var import_child_process = require("child_process");
|
|
377
423
|
|
|
378
424
|
// src/transcript-parser-v2.ts
|
|
425
|
+
var log4 = createLogger("transcript-parser-v2");
|
|
426
|
+
var DEBUG_PARSE = process.env.RAGENT_DEBUG_TRANSCRIPT === "1";
|
|
427
|
+
function debugTranscriptParse(parser, line, error) {
|
|
428
|
+
if (!DEBUG_PARSE) return;
|
|
429
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
430
|
+
const preview = line.length > 120 ? line.slice(0, 117) + "\u2026" : line;
|
|
431
|
+
log4.debug("skipping unparseable line", { parser, error: message, preview });
|
|
432
|
+
}
|
|
379
433
|
var blockCounter = 0;
|
|
380
434
|
function genBlockId(prefix) {
|
|
381
435
|
return `${prefix}-${Date.now()}-${(++blockCounter).toString(36)}`;
|
|
@@ -421,7 +475,8 @@ var ClaudeCodeParserV2 = class {
|
|
|
421
475
|
let obj;
|
|
422
476
|
try {
|
|
423
477
|
obj = JSON.parse(line);
|
|
424
|
-
} catch {
|
|
478
|
+
} catch (error) {
|
|
479
|
+
debugTranscriptParse("claude", line, error);
|
|
425
480
|
return [];
|
|
426
481
|
}
|
|
427
482
|
if (!obj.message?.content) return [];
|
|
@@ -613,7 +668,8 @@ var CodexCliParserV2 = class {
|
|
|
613
668
|
let obj;
|
|
614
669
|
try {
|
|
615
670
|
obj = JSON.parse(line);
|
|
616
|
-
} catch {
|
|
671
|
+
} catch (error) {
|
|
672
|
+
debugTranscriptParse("codex", line, error);
|
|
617
673
|
return [];
|
|
618
674
|
}
|
|
619
675
|
const item = obj.response_item;
|
|
@@ -662,10 +718,10 @@ function getParserV2(agentType) {
|
|
|
662
718
|
return factory ? factory() : void 0;
|
|
663
719
|
}
|
|
664
720
|
|
|
665
|
-
//
|
|
721
|
+
// ../packages/transcript-protocol/src/protocol.ts
|
|
666
722
|
var TRANSCRIPT_SCHEMA_VERSION = 2;
|
|
667
723
|
|
|
668
|
-
//
|
|
724
|
+
// ../packages/transcript-protocol/src/reducer.ts
|
|
669
725
|
function createEmptySnapshot(sessionId) {
|
|
670
726
|
return {
|
|
671
727
|
schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
|
|
@@ -868,6 +924,7 @@ function trimSnapshot(snapshot, maxTurns = MAX_SNAPSHOT_TURNS) {
|
|
|
868
924
|
}
|
|
869
925
|
|
|
870
926
|
// src/transcript-watcher.ts
|
|
927
|
+
var log5 = createLogger("transcript-watcher");
|
|
871
928
|
var ClaudeCodeParser = class {
|
|
872
929
|
name = "claude-code";
|
|
873
930
|
/** Maps tool_use id → tool name for matching results back to tools. */
|
|
@@ -1211,12 +1268,18 @@ var TranscriptWatcher = class {
|
|
|
1211
1268
|
addSubscriber() {
|
|
1212
1269
|
this.subscriberCount++;
|
|
1213
1270
|
}
|
|
1214
|
-
/**
|
|
1271
|
+
/**
|
|
1272
|
+
* Remove a subscriber. Stops watching when the last subscriber leaves.
|
|
1273
|
+
* Returns the remaining subscriber count so callers can synchronize
|
|
1274
|
+
* manager-side bookkeeping (e.g. removing the entry from the active map
|
|
1275
|
+
* only on the last disconnect).
|
|
1276
|
+
*/
|
|
1215
1277
|
removeSubscriber() {
|
|
1216
1278
|
this.subscriberCount = Math.max(0, this.subscriberCount - 1);
|
|
1217
1279
|
if (this.subscriberCount === 0) {
|
|
1218
1280
|
this.stop();
|
|
1219
1281
|
}
|
|
1282
|
+
return this.subscriberCount;
|
|
1220
1283
|
}
|
|
1221
1284
|
/** Stop watching. */
|
|
1222
1285
|
stop() {
|
|
@@ -1379,15 +1442,15 @@ var TranscriptWatcherManager = class {
|
|
|
1379
1442
|
}
|
|
1380
1443
|
const parser = getParser(agentType);
|
|
1381
1444
|
if (!parser) {
|
|
1382
|
-
|
|
1445
|
+
log5.info("no transcript parser for agent type", { agentType });
|
|
1383
1446
|
return { ok: false, reason: "unsupported_agent" };
|
|
1384
1447
|
}
|
|
1385
1448
|
const filePath = discoverTranscriptFile(sessionId, agentType);
|
|
1386
1449
|
if (!filePath) {
|
|
1387
|
-
|
|
1450
|
+
log5.info("no transcript file found", { sessionId });
|
|
1388
1451
|
return { ok: false, reason: "no_transcript" };
|
|
1389
1452
|
}
|
|
1390
|
-
|
|
1453
|
+
log5.info("watching transcript", { path: filePath, parser: parser.name });
|
|
1391
1454
|
const v2Parser = getParserV2(agentType);
|
|
1392
1455
|
const watcher = new TranscriptWatcher(
|
|
1393
1456
|
filePath,
|
|
@@ -1400,14 +1463,14 @@ var TranscriptWatcherManager = class {
|
|
|
1400
1463
|
this.sendOpsFn(sessionId, ops, seq);
|
|
1401
1464
|
} : void 0,
|
|
1402
1465
|
onError: (error) => {
|
|
1403
|
-
|
|
1466
|
+
log5.warn("transcript watcher error", { sessionId, error: String(error) });
|
|
1404
1467
|
}
|
|
1405
1468
|
},
|
|
1406
1469
|
v2Parser,
|
|
1407
1470
|
sessionId
|
|
1408
1471
|
);
|
|
1409
1472
|
if (!watcher.start()) {
|
|
1410
|
-
|
|
1473
|
+
log5.warn("failed to start watching", { path: filePath });
|
|
1411
1474
|
return { ok: false, reason: "watch_failed" };
|
|
1412
1475
|
}
|
|
1413
1476
|
this.active.set(sessionId, { watcher, filePath, agentType: agentType ?? "" });
|
|
@@ -1419,10 +1482,17 @@ var TranscriptWatcherManager = class {
|
|
|
1419
1482
|
disableMarkdown(sessionId) {
|
|
1420
1483
|
const session = this.active.get(sessionId);
|
|
1421
1484
|
if (!session) return;
|
|
1422
|
-
session.watcher.removeSubscriber();
|
|
1485
|
+
const remaining = session.watcher.removeSubscriber();
|
|
1486
|
+
if (remaining > 0) {
|
|
1487
|
+
log5.info("viewer left transcript watcher; other viewers still subscribed", {
|
|
1488
|
+
sessionId,
|
|
1489
|
+
remaining
|
|
1490
|
+
});
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1423
1493
|
this.active.delete(sessionId);
|
|
1424
1494
|
this.stopRediscovery(sessionId);
|
|
1425
|
-
|
|
1495
|
+
log5.info("stopped watching transcript", { sessionId });
|
|
1426
1496
|
}
|
|
1427
1497
|
/** Periodically check if the transcript file has changed (new conversation). */
|
|
1428
1498
|
startRediscovery(sessionId, agentType) {
|
|
@@ -1435,7 +1505,7 @@ var TranscriptWatcherManager = class {
|
|
|
1435
1505
|
}
|
|
1436
1506
|
const newPath = discoverTranscriptFile(sessionId, agentType);
|
|
1437
1507
|
if (!newPath || newPath === session.filePath) return;
|
|
1438
|
-
|
|
1508
|
+
log5.info("transcript file changed", { sessionId, newPath });
|
|
1439
1509
|
session.watcher.stop();
|
|
1440
1510
|
const parser = getParser(agentType);
|
|
1441
1511
|
if (!parser) return;
|
|
@@ -1451,14 +1521,14 @@ var TranscriptWatcherManager = class {
|
|
|
1451
1521
|
this.sendOpsFn(sessionId, ops, seq);
|
|
1452
1522
|
} : void 0,
|
|
1453
1523
|
onError: (error) => {
|
|
1454
|
-
|
|
1524
|
+
log5.warn("transcript watcher error", { sessionId, error: String(error) });
|
|
1455
1525
|
}
|
|
1456
1526
|
},
|
|
1457
1527
|
newV2Parser,
|
|
1458
1528
|
sessionId
|
|
1459
1529
|
);
|
|
1460
1530
|
if (!newWatcher.start()) {
|
|
1461
|
-
|
|
1531
|
+
log5.warn("failed to start watching new transcript", { newPath });
|
|
1462
1532
|
return;
|
|
1463
1533
|
}
|
|
1464
1534
|
this.sendSnapshotFn(sessionId, [], 0);
|
|
@@ -1494,7 +1564,7 @@ var TranscriptWatcherManager = class {
|
|
|
1494
1564
|
for (const [sessionId, session] of this.active) {
|
|
1495
1565
|
session.watcher.stop();
|
|
1496
1566
|
this.stopRediscovery(sessionId);
|
|
1497
|
-
|
|
1567
|
+
log5.info("stopped transcript watcher", { sessionId });
|
|
1498
1568
|
}
|
|
1499
1569
|
this.active.clear();
|
|
1500
1570
|
this.rediscoveryTimers.clear();
|
|
@@ -1990,6 +2060,7 @@ async function getChildAgentInfo(parentPid, visited = /* @__PURE__ */ new Set())
|
|
|
1990
2060
|
}
|
|
1991
2061
|
|
|
1992
2062
|
// src/auth.ts
|
|
2063
|
+
var log6 = createLogger("auth");
|
|
1993
2064
|
var AuthError = class extends Error {
|
|
1994
2065
|
constructor(message) {
|
|
1995
2066
|
super(message);
|
|
@@ -2007,7 +2078,7 @@ function decodeJwtExp(token) {
|
|
|
2007
2078
|
}
|
|
2008
2079
|
}
|
|
2009
2080
|
async function startSessionWithMachineSecret(params) {
|
|
2010
|
-
|
|
2081
|
+
log6.info("attempting session recovery with machine credential");
|
|
2011
2082
|
const response = await fetch(`${params.portal}/api/agent/session/start`, {
|
|
2012
2083
|
method: "POST",
|
|
2013
2084
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2040,7 +2111,7 @@ async function startSessionWithMachineSecret(params) {
|
|
|
2040
2111
|
patch.refreshExpiresAt = data.refreshExpiresAt || "";
|
|
2041
2112
|
}
|
|
2042
2113
|
saveConfigPatch(patch);
|
|
2043
|
-
|
|
2114
|
+
log6.info("session recovered via machine credential");
|
|
2044
2115
|
return data.agentToken;
|
|
2045
2116
|
}
|
|
2046
2117
|
async function refreshTokenIfNeeded(params) {
|
|
@@ -2049,11 +2120,9 @@ async function refreshTokenIfNeeded(params) {
|
|
|
2049
2120
|
const exp = decodeJwtExp(params.agentToken);
|
|
2050
2121
|
if (!exp) return params.agentToken;
|
|
2051
2122
|
const remainingSeconds = exp - Math.floor(Date.now() / 1e3);
|
|
2052
|
-
const
|
|
2053
|
-
if (remainingSeconds >
|
|
2054
|
-
|
|
2055
|
-
`[rAgent] Token expires in ${Math.round(remainingSeconds / 60)}m \u2014 refreshing...`
|
|
2056
|
-
);
|
|
2123
|
+
const threshold2 = remainingSeconds < 3600 ? 120 : 3600;
|
|
2124
|
+
if (remainingSeconds > threshold2) return params.agentToken;
|
|
2125
|
+
log6.info("token expiring \u2014 refreshing", { minutesRemaining: Math.round(remainingSeconds / 60) });
|
|
2057
2126
|
try {
|
|
2058
2127
|
const headers = { "Content-Type": "application/json" };
|
|
2059
2128
|
let body = {};
|
|
@@ -2069,9 +2138,7 @@ async function refreshTokenIfNeeded(params) {
|
|
|
2069
2138
|
});
|
|
2070
2139
|
if (!response.ok) {
|
|
2071
2140
|
const errorData = await response.json().catch(() => ({}));
|
|
2072
|
-
|
|
2073
|
-
`[rAgent] Token refresh failed: ${errorData.error || response.status}`
|
|
2074
|
-
);
|
|
2141
|
+
log6.warn("token refresh failed", { error: errorData.error || response.status });
|
|
2075
2142
|
if (config.machineSecret && config.hostId) {
|
|
2076
2143
|
try {
|
|
2077
2144
|
return await startSessionWithMachineSecret({
|
|
@@ -2082,7 +2149,7 @@ async function refreshTokenIfNeeded(params) {
|
|
|
2082
2149
|
} catch (mcError) {
|
|
2083
2150
|
if (mcError instanceof AuthError) throw mcError;
|
|
2084
2151
|
const mcMessage = mcError instanceof Error ? mcError.message : String(mcError);
|
|
2085
|
-
|
|
2152
|
+
log6.warn("machine credential recovery failed", { error: mcMessage });
|
|
2086
2153
|
}
|
|
2087
2154
|
}
|
|
2088
2155
|
return params.agentToken;
|
|
@@ -2101,12 +2168,12 @@ async function refreshTokenIfNeeded(params) {
|
|
|
2101
2168
|
patch.machineSecret = data.machineSecret;
|
|
2102
2169
|
}
|
|
2103
2170
|
saveConfigPatch(patch);
|
|
2104
|
-
|
|
2171
|
+
log6.info("token refreshed successfully");
|
|
2105
2172
|
return data.agentToken;
|
|
2106
2173
|
} catch (error) {
|
|
2107
2174
|
if (error instanceof AuthError) throw error;
|
|
2108
2175
|
const message = error instanceof Error ? error.message : String(error);
|
|
2109
|
-
|
|
2176
|
+
log6.warn("token refresh error", { error: message });
|
|
2110
2177
|
return params.agentToken;
|
|
2111
2178
|
}
|
|
2112
2179
|
}
|
|
@@ -2169,7 +2236,7 @@ async function deregisterHost(params) {
|
|
|
2169
2236
|
body: JSON.stringify({})
|
|
2170
2237
|
});
|
|
2171
2238
|
if (response.status === 401 || response.status === 403) {
|
|
2172
|
-
|
|
2239
|
+
log6.warn("token expired \u2014 could not deregister from server. Host may remain visible in dashboard until manually removed");
|
|
2173
2240
|
return false;
|
|
2174
2241
|
}
|
|
2175
2242
|
return response.ok;
|
|
@@ -2275,13 +2342,42 @@ function redactSecrets(str) {
|
|
|
2275
2342
|
}
|
|
2276
2343
|
return result;
|
|
2277
2344
|
}
|
|
2345
|
+
var streamingTails = /* @__PURE__ */ new Map();
|
|
2346
|
+
var MAX_STREAMING_TAIL = 1024;
|
|
2347
|
+
var SECRET_PREFIX_RE = /(?:AKIA[0-9A-Z]*|gh[ps]_[A-Za-z0-9_]*|sk_(?:live|test)_[A-Za-z0-9]*|eyJ[A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]*){0,2})$/;
|
|
2348
|
+
function redactStreamingChunk(sessionId, chunk) {
|
|
2349
|
+
if (!redactionEnabled) return chunk;
|
|
2350
|
+
const prevTail = streamingTails.get(sessionId) ?? "";
|
|
2351
|
+
const combined = prevTail + chunk;
|
|
2352
|
+
const redacted = redactSecrets(combined);
|
|
2353
|
+
const prefix = SECRET_PREFIX_RE.exec(redacted);
|
|
2354
|
+
if (prefix && prefix.index + prefix[0].length === redacted.length && redacted.length - prefix.index <= MAX_STREAMING_TAIL) {
|
|
2355
|
+
streamingTails.set(sessionId, redacted.slice(prefix.index));
|
|
2356
|
+
return redacted.slice(0, prefix.index);
|
|
2357
|
+
}
|
|
2358
|
+
streamingTails.delete(sessionId);
|
|
2359
|
+
return redacted;
|
|
2360
|
+
}
|
|
2361
|
+
function flushStreamingRedaction(sessionId) {
|
|
2362
|
+
const tail = streamingTails.get(sessionId) ?? "";
|
|
2363
|
+
streamingTails.delete(sessionId);
|
|
2364
|
+
return redactionEnabled ? redactSecrets(tail) : tail;
|
|
2365
|
+
}
|
|
2278
2366
|
var pendingQueue = [];
|
|
2279
2367
|
var drainTimer = null;
|
|
2280
2368
|
var drainWs = null;
|
|
2281
2369
|
var droppedFrames = 0;
|
|
2370
|
+
var PRESSURE_HIGH = 0.75;
|
|
2371
|
+
var PRESSURE_LOW = 0.25;
|
|
2372
|
+
var pressureIsHigh = false;
|
|
2373
|
+
function getQueuePressure() {
|
|
2374
|
+
const level = pendingQueue.length / MAX_PENDING_QUEUE;
|
|
2375
|
+
if (level >= PRESSURE_HIGH) pressureIsHigh = true;
|
|
2376
|
+
else if (level < PRESSURE_LOW) pressureIsHigh = false;
|
|
2377
|
+
return { level, isHigh: pressureIsHigh };
|
|
2378
|
+
}
|
|
2282
2379
|
function drainQueue() {
|
|
2283
2380
|
if (!drainWs || drainWs.readyState !== import_ws.default.OPEN) {
|
|
2284
|
-
pendingQueue.length = 0;
|
|
2285
2381
|
stopDrainTimer();
|
|
2286
2382
|
return;
|
|
2287
2383
|
}
|
|
@@ -2330,6 +2426,7 @@ function sendToGroup(ws, group, data) {
|
|
|
2330
2426
|
}
|
|
2331
2427
|
if (pendingQueue.length > 0) {
|
|
2332
2428
|
pendingQueue.push(frame);
|
|
2429
|
+
drainWs = ws;
|
|
2333
2430
|
drainQueue();
|
|
2334
2431
|
return;
|
|
2335
2432
|
}
|
|
@@ -2362,6 +2459,8 @@ var import_node_path = require("path");
|
|
|
2362
2459
|
var import_node_os = require("os");
|
|
2363
2460
|
var import_node_string_decoder = require("string_decoder");
|
|
2364
2461
|
var pty = __toESM(require("node-pty"));
|
|
2462
|
+
var log7 = createLogger("session-streamer");
|
|
2463
|
+
var BACKPRESSURE_RESUME_CHECK_MS = 100;
|
|
2365
2464
|
var STOP_DEBOUNCE_MS = 2e3;
|
|
2366
2465
|
var MAX_CONCURRENT_STREAMS = 20;
|
|
2367
2466
|
var MAX_SESSION_BUFFER_BYTES = 64 * 1024;
|
|
@@ -2427,10 +2526,54 @@ var SessionStreamer = class {
|
|
|
2427
2526
|
*/
|
|
2428
2527
|
disconnectBuffers = /* @__PURE__ */ new Map();
|
|
2429
2528
|
disconnectBufferBytes = /* @__PURE__ */ new Map();
|
|
2529
|
+
/** Timer that resumes paused PTYs once the WS queue drains. See #332. */
|
|
2530
|
+
backpressureResumeTimer = null;
|
|
2430
2531
|
constructor(sendFn, onStreamStopped) {
|
|
2431
2532
|
this.sendFn = sendFn;
|
|
2432
2533
|
this.onStreamStopped = onStreamStopped;
|
|
2433
2534
|
this.cleanupStaleStreams();
|
|
2535
|
+
this.startBackpressureResumeTimer();
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* When the WS queue is saturated, PTY onData stops firing after we call
|
|
2539
|
+
* ptyProc.pause(). We need an independent loop to check pressure and
|
|
2540
|
+
* resume each paused PTY once the queue drains. See #332.
|
|
2541
|
+
*/
|
|
2542
|
+
startBackpressureResumeTimer() {
|
|
2543
|
+
if (this.backpressureResumeTimer) return;
|
|
2544
|
+
this.backpressureResumeTimer = setInterval(() => {
|
|
2545
|
+
const pressure = getQueuePressure();
|
|
2546
|
+
if (pressure.isHigh) return;
|
|
2547
|
+
for (const stream of this.active.values()) {
|
|
2548
|
+
if (stream.ptyPaused && stream.ptyProc && !stream.stopped) {
|
|
2549
|
+
try {
|
|
2550
|
+
stream.ptyProc.resume();
|
|
2551
|
+
stream.ptyPaused = false;
|
|
2552
|
+
log7.info("pty-attach resumed after WS queue drained", { sessionId: stream.sessionId });
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}, BACKPRESSURE_RESUME_CHECK_MS);
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Pause a PTY-attach stream when WS queue pressure is high. Only applies
|
|
2561
|
+
* to `pty-attach` streams — tmux-pipe (FIFO cat) and process-trace
|
|
2562
|
+
* (strace) don't have a pause primitive in this implementation.
|
|
2563
|
+
*/
|
|
2564
|
+
applyBackpressure(stream) {
|
|
2565
|
+
if (stream.streamType !== "pty-attach" || !stream.ptyProc || stream.ptyPaused) return;
|
|
2566
|
+
const pressure = getQueuePressure();
|
|
2567
|
+
if (!pressure.isHigh) return;
|
|
2568
|
+
try {
|
|
2569
|
+
stream.ptyProc.pause();
|
|
2570
|
+
stream.ptyPaused = true;
|
|
2571
|
+
log7.warn("pty-attach paused under WS backpressure", {
|
|
2572
|
+
sessionId: stream.sessionId,
|
|
2573
|
+
queueLevel: pressure.level.toFixed(2)
|
|
2574
|
+
});
|
|
2575
|
+
} catch {
|
|
2576
|
+
}
|
|
2434
2577
|
}
|
|
2435
2578
|
/**
|
|
2436
2579
|
* Remove orphaned FIFO directories left behind by a previous crash.
|
|
@@ -2449,7 +2592,7 @@ var SessionStreamer = class {
|
|
|
2449
2592
|
}
|
|
2450
2593
|
}
|
|
2451
2594
|
if (entries.some((e) => e.startsWith("s-"))) {
|
|
2452
|
-
|
|
2595
|
+
log7.info("cleaned up stale stream directories from previous run");
|
|
2453
2596
|
}
|
|
2454
2597
|
} catch {
|
|
2455
2598
|
}
|
|
@@ -2467,7 +2610,7 @@ var SessionStreamer = class {
|
|
|
2467
2610
|
if (viewerKey) {
|
|
2468
2611
|
this.active.get(sessionId)?.viewerIds.add(viewerKey);
|
|
2469
2612
|
}
|
|
2470
|
-
|
|
2613
|
+
log7.info("cancelled pending stop (re-mounted)", { sessionId });
|
|
2471
2614
|
return true;
|
|
2472
2615
|
}
|
|
2473
2616
|
}
|
|
@@ -2478,9 +2621,10 @@ var SessionStreamer = class {
|
|
|
2478
2621
|
return true;
|
|
2479
2622
|
}
|
|
2480
2623
|
if (this.active.size >= MAX_CONCURRENT_STREAMS) {
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2624
|
+
log7.warn("stream limit reached; rejecting", {
|
|
2625
|
+
limit: MAX_CONCURRENT_STREAMS,
|
|
2626
|
+
sessionId
|
|
2627
|
+
});
|
|
2484
2628
|
return false;
|
|
2485
2629
|
}
|
|
2486
2630
|
if (sessionId.startsWith("tmux:")) {
|
|
@@ -2508,7 +2652,7 @@ var SessionStreamer = class {
|
|
|
2508
2652
|
stream.ptyProc.write(data);
|
|
2509
2653
|
} catch (error) {
|
|
2510
2654
|
const message = error instanceof Error ? error.message : String(error);
|
|
2511
|
-
|
|
2655
|
+
log7.warn("failed to write input", { sessionId, error: message });
|
|
2512
2656
|
}
|
|
2513
2657
|
}
|
|
2514
2658
|
/**
|
|
@@ -2522,7 +2666,7 @@ var SessionStreamer = class {
|
|
|
2522
2666
|
} catch (error) {
|
|
2523
2667
|
const message = error instanceof Error ? error.message : String(error);
|
|
2524
2668
|
if (!message.includes("EBADF")) {
|
|
2525
|
-
|
|
2669
|
+
log7.warn("resize failed", { sessionId, error: message });
|
|
2526
2670
|
}
|
|
2527
2671
|
}
|
|
2528
2672
|
}
|
|
@@ -2548,7 +2692,7 @@ var SessionStreamer = class {
|
|
|
2548
2692
|
this.active.delete(sessionId);
|
|
2549
2693
|
this.disconnectBuffers.delete(sessionId);
|
|
2550
2694
|
this.disconnectBufferBytes.delete(sessionId);
|
|
2551
|
-
|
|
2695
|
+
log7.info("stopped streaming", { sessionId });
|
|
2552
2696
|
}, STOP_DEBOUNCE_MS);
|
|
2553
2697
|
this.pendingStops.set(sessionId, timer);
|
|
2554
2698
|
}
|
|
@@ -2568,11 +2712,15 @@ var SessionStreamer = class {
|
|
|
2568
2712
|
this.pendingStops.clear();
|
|
2569
2713
|
for (const [id, stream] of this.active) {
|
|
2570
2714
|
this.cleanupStream(stream);
|
|
2571
|
-
|
|
2715
|
+
log7.info("stopped streaming", { sessionId: id });
|
|
2572
2716
|
}
|
|
2573
2717
|
this.active.clear();
|
|
2574
2718
|
this.disconnectBuffers.clear();
|
|
2575
2719
|
this.disconnectBufferBytes.clear();
|
|
2720
|
+
if (this.backpressureResumeTimer) {
|
|
2721
|
+
clearInterval(this.backpressureResumeTimer);
|
|
2722
|
+
this.backpressureResumeTimer = null;
|
|
2723
|
+
}
|
|
2576
2724
|
}
|
|
2577
2725
|
/**
|
|
2578
2726
|
* Get the number of active streams.
|
|
@@ -2608,7 +2756,9 @@ var SessionStreamer = class {
|
|
|
2608
2756
|
currentBytes -= Buffer.byteLength(evicted, "utf8");
|
|
2609
2757
|
}
|
|
2610
2758
|
if (dataBytes > MAX_SESSION_BUFFER_BYTES) {
|
|
2611
|
-
const
|
|
2759
|
+
const buf = Buffer.from(data, "utf8");
|
|
2760
|
+
const tailBuf = buf.subarray(buf.length - MAX_SESSION_BUFFER_BYTES);
|
|
2761
|
+
const tail = tailBuf.toString("utf8");
|
|
2612
2762
|
chunks.push(tail);
|
|
2613
2763
|
this.disconnectBufferBytes.set(sessionId, Buffer.byteLength(tail, "utf8"));
|
|
2614
2764
|
return;
|
|
@@ -2695,13 +2845,13 @@ var SessionStreamer = class {
|
|
|
2695
2845
|
}
|
|
2696
2846
|
stream.initializing = false;
|
|
2697
2847
|
stream.initBuffer = [];
|
|
2698
|
-
|
|
2848
|
+
log7.info("resync capture", { sessionId });
|
|
2699
2849
|
return true;
|
|
2700
2850
|
} catch (error) {
|
|
2701
2851
|
stream.initializing = false;
|
|
2702
2852
|
stream.initBuffer = [];
|
|
2703
2853
|
const message = error instanceof Error ? error.message : String(error);
|
|
2704
|
-
|
|
2854
|
+
log7.warn("failed to resync stream", { sessionId, error: message });
|
|
2705
2855
|
return false;
|
|
2706
2856
|
}
|
|
2707
2857
|
}
|
|
@@ -2754,7 +2904,7 @@ var SessionStreamer = class {
|
|
|
2754
2904
|
} catch (error) {
|
|
2755
2905
|
this.cleanupStream(stream);
|
|
2756
2906
|
const message = error instanceof Error ? error.message : String(error);
|
|
2757
|
-
|
|
2907
|
+
log7.warn("failed pipe-pane", { sessionId, error: message });
|
|
2758
2908
|
return false;
|
|
2759
2909
|
}
|
|
2760
2910
|
const catProc = (0, import_node_child_process2.spawn)("cat", [fifoPath], { env: cleanEnv, stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -2776,7 +2926,7 @@ var SessionStreamer = class {
|
|
|
2776
2926
|
this.cleanupStream(stream);
|
|
2777
2927
|
this.active.delete(sessionId);
|
|
2778
2928
|
this.pendingStops.delete(sessionId);
|
|
2779
|
-
|
|
2929
|
+
log7.info("session stream ended", { sessionId });
|
|
2780
2930
|
this.onStreamStopped?.(sessionId);
|
|
2781
2931
|
}
|
|
2782
2932
|
});
|
|
@@ -2836,11 +2986,11 @@ var SessionStreamer = class {
|
|
|
2836
2986
|
}
|
|
2837
2987
|
stream.initializing = false;
|
|
2838
2988
|
stream.initBuffer = [];
|
|
2839
|
-
|
|
2989
|
+
log7.info("started streaming (tmux)", { sessionId, pane: paneTarget });
|
|
2840
2990
|
return true;
|
|
2841
2991
|
} catch (error) {
|
|
2842
2992
|
const message = error instanceof Error ? error.message : String(error);
|
|
2843
|
-
|
|
2993
|
+
log7.warn("failed to start stream", { sessionId, error: message });
|
|
2844
2994
|
return false;
|
|
2845
2995
|
}
|
|
2846
2996
|
}
|
|
@@ -2877,6 +3027,7 @@ var SessionStreamer = class {
|
|
|
2877
3027
|
proc.onData((data) => {
|
|
2878
3028
|
if (!stream.stopped) {
|
|
2879
3029
|
this.sendFn(sessionId, data);
|
|
3030
|
+
this.applyBackpressure(stream);
|
|
2880
3031
|
}
|
|
2881
3032
|
});
|
|
2882
3033
|
proc.onExit(() => {
|
|
@@ -2884,15 +3035,15 @@ var SessionStreamer = class {
|
|
|
2884
3035
|
stream.stopped = true;
|
|
2885
3036
|
this.active.delete(sessionId);
|
|
2886
3037
|
this.pendingStops.delete(sessionId);
|
|
2887
|
-
|
|
3038
|
+
log7.info("session stream ended", { sessionId });
|
|
2888
3039
|
this.onStreamStopped?.(sessionId);
|
|
2889
3040
|
}
|
|
2890
3041
|
});
|
|
2891
|
-
|
|
3042
|
+
log7.info("started streaming (screen)", { sessionId, screen: sessionName });
|
|
2892
3043
|
return true;
|
|
2893
3044
|
} catch (error) {
|
|
2894
3045
|
const message = error instanceof Error ? error.message : String(error);
|
|
2895
|
-
|
|
3046
|
+
log7.warn("failed to start screen stream", { sessionId, error: message });
|
|
2896
3047
|
return false;
|
|
2897
3048
|
}
|
|
2898
3049
|
}
|
|
@@ -2929,6 +3080,7 @@ var SessionStreamer = class {
|
|
|
2929
3080
|
proc.onData((data) => {
|
|
2930
3081
|
if (!stream.stopped) {
|
|
2931
3082
|
this.sendFn(sessionId, data);
|
|
3083
|
+
this.applyBackpressure(stream);
|
|
2932
3084
|
}
|
|
2933
3085
|
});
|
|
2934
3086
|
proc.onExit(() => {
|
|
@@ -2936,15 +3088,15 @@ var SessionStreamer = class {
|
|
|
2936
3088
|
stream.stopped = true;
|
|
2937
3089
|
this.active.delete(sessionId);
|
|
2938
3090
|
this.pendingStops.delete(sessionId);
|
|
2939
|
-
|
|
3091
|
+
log7.info("session stream ended", { sessionId });
|
|
2940
3092
|
this.onStreamStopped?.(sessionId);
|
|
2941
3093
|
}
|
|
2942
3094
|
});
|
|
2943
|
-
|
|
3095
|
+
log7.info("started streaming (zellij)", { sessionId, zellij: sessionName });
|
|
2944
3096
|
return true;
|
|
2945
3097
|
} catch (error) {
|
|
2946
3098
|
const message = error instanceof Error ? error.message : String(error);
|
|
2947
|
-
|
|
3099
|
+
log7.warn("failed to start zellij stream", { sessionId, error: message });
|
|
2948
3100
|
return false;
|
|
2949
3101
|
}
|
|
2950
3102
|
}
|
|
@@ -2955,14 +3107,14 @@ var SessionStreamer = class {
|
|
|
2955
3107
|
const pid = parseProcessPid2(sessionId);
|
|
2956
3108
|
if (!pid) return false;
|
|
2957
3109
|
if (!(0, import_node_fs.existsSync)(`/proc/${pid}`)) {
|
|
2958
|
-
|
|
3110
|
+
log7.warn("process does not exist (no /proc entry)", { pid });
|
|
2959
3111
|
this.sendFn(sessionId, "\r\n[rAgent] Process is no longer running. It will be removed on next sync.\r\n");
|
|
2960
3112
|
return false;
|
|
2961
3113
|
}
|
|
2962
3114
|
try {
|
|
2963
3115
|
const stat = (0, import_node_fs.readFileSync)(`/proc/${pid}/stat`, "utf8");
|
|
2964
3116
|
if (stat.includes(") Z")) {
|
|
2965
|
-
|
|
3117
|
+
log7.warn("process is a zombie", { pid });
|
|
2966
3118
|
this.sendFn(sessionId, "\r\n[rAgent] Process has exited (zombie). It will be removed on next sync.\r\n");
|
|
2967
3119
|
return false;
|
|
2968
3120
|
}
|
|
@@ -3035,14 +3187,14 @@ var SessionStreamer = class {
|
|
|
3035
3187
|
stream.stopped = true;
|
|
3036
3188
|
this.active.delete(sessionId);
|
|
3037
3189
|
this.pendingStops.delete(sessionId);
|
|
3038
|
-
|
|
3190
|
+
log7.info("session stream ended", { sessionId });
|
|
3039
3191
|
this.onStreamStopped?.(sessionId);
|
|
3040
3192
|
});
|
|
3041
|
-
|
|
3193
|
+
log7.info("started streaming (process trace)", { sessionId, pid });
|
|
3042
3194
|
return true;
|
|
3043
3195
|
} catch (error) {
|
|
3044
3196
|
const message = error instanceof Error ? error.message : String(error);
|
|
3045
|
-
|
|
3197
|
+
log7.warn("failed to start process stream", { sessionId, error: message });
|
|
3046
3198
|
return false;
|
|
3047
3199
|
}
|
|
3048
3200
|
}
|
|
@@ -3093,25 +3245,34 @@ var SessionStreamer = class {
|
|
|
3093
3245
|
|
|
3094
3246
|
// src/crypto-channel.ts
|
|
3095
3247
|
var import_node_crypto = require("crypto");
|
|
3248
|
+
var log8 = createLogger("crypto-channel");
|
|
3249
|
+
var AAD_BYTES = 4;
|
|
3096
3250
|
function deriveAesKey(sessionKey) {
|
|
3097
3251
|
return Buffer.from(sessionKey.slice(0, 64), "hex");
|
|
3098
3252
|
}
|
|
3099
|
-
function
|
|
3253
|
+
function seqToAad(seq) {
|
|
3254
|
+
if (!Number.isInteger(seq) || seq < 0 || seq > 4294967295) {
|
|
3255
|
+
throw new Error(`Invalid seq ${seq}: must be a UInt32`);
|
|
3256
|
+
}
|
|
3257
|
+
const buf = Buffer.alloc(AAD_BYTES);
|
|
3258
|
+
buf.writeUInt32BE(seq, 0);
|
|
3259
|
+
return buf;
|
|
3260
|
+
}
|
|
3261
|
+
function encryptPayload(data, sessionKey, seq) {
|
|
3100
3262
|
const key = deriveAesKey(sessionKey);
|
|
3101
3263
|
const iv = (0, import_node_crypto.randomBytes)(12);
|
|
3102
3264
|
const cipher = (0, import_node_crypto.createCipheriv)("aes-256-gcm", key, iv);
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
cipher.final()
|
|
3106
|
-
]);
|
|
3265
|
+
cipher.setAAD(seqToAad(seq));
|
|
3266
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf-8"), cipher.final()]);
|
|
3107
3267
|
const authTag = cipher.getAuthTag();
|
|
3108
3268
|
const combined = Buffer.concat([encrypted, authTag]);
|
|
3109
3269
|
return {
|
|
3110
3270
|
enc: combined.toString("base64"),
|
|
3111
|
-
iv: iv.toString("base64")
|
|
3271
|
+
iv: iv.toString("base64"),
|
|
3272
|
+
seq
|
|
3112
3273
|
};
|
|
3113
3274
|
}
|
|
3114
|
-
function decryptPayload(enc, iv, sessionKey) {
|
|
3275
|
+
function decryptPayload(enc, iv, sessionKey, seq) {
|
|
3115
3276
|
try {
|
|
3116
3277
|
const key = deriveAesKey(sessionKey);
|
|
3117
3278
|
const ivBuf = Buffer.from(iv, "base64");
|
|
@@ -3120,20 +3281,47 @@ function decryptPayload(enc, iv, sessionKey) {
|
|
|
3120
3281
|
const ciphertext = combined.subarray(0, combined.length - 16);
|
|
3121
3282
|
const authTag = combined.subarray(combined.length - 16);
|
|
3122
3283
|
const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", key, ivBuf);
|
|
3284
|
+
decipher.setAAD(seqToAad(seq));
|
|
3123
3285
|
decipher.setAuthTag(authTag);
|
|
3124
|
-
const decrypted = Buffer.concat([
|
|
3125
|
-
decipher.update(ciphertext),
|
|
3126
|
-
decipher.final()
|
|
3127
|
-
]);
|
|
3286
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
3128
3287
|
return decrypted.toString("utf-8");
|
|
3129
|
-
} catch {
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3290
|
+
log8.warn("decrypt failed", { seq, encBytes: enc.length, error: message });
|
|
3130
3291
|
return null;
|
|
3131
3292
|
}
|
|
3132
3293
|
}
|
|
3294
|
+
var SequenceTracker = class {
|
|
3295
|
+
last = 0;
|
|
3296
|
+
/** Sender: produce the next outgoing seq. */
|
|
3297
|
+
next() {
|
|
3298
|
+
if (this.last >= 4294967295) {
|
|
3299
|
+
throw new Error("Sequence counter overflow; channel must re-handshake.");
|
|
3300
|
+
}
|
|
3301
|
+
this.last += 1;
|
|
3302
|
+
return this.last;
|
|
3303
|
+
}
|
|
3304
|
+
/** Receiver: accept if strictly greater than last-seen; else reject. */
|
|
3305
|
+
accept(seq) {
|
|
3306
|
+
if (!Number.isInteger(seq) || seq < 1 || seq > 4294967295) return false;
|
|
3307
|
+
if (seq <= this.last) return false;
|
|
3308
|
+
this.last = seq;
|
|
3309
|
+
return true;
|
|
3310
|
+
}
|
|
3311
|
+
/** Reset to zero (call on new handshake / sessionKey change). */
|
|
3312
|
+
reset() {
|
|
3313
|
+
this.last = 0;
|
|
3314
|
+
}
|
|
3315
|
+
/** Inspect the last-seen seq (0 means "nothing yet"). */
|
|
3316
|
+
get lastSeq() {
|
|
3317
|
+
return this.last;
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3133
3320
|
|
|
3134
3321
|
// src/pty.ts
|
|
3135
3322
|
var import_node_child_process3 = require("child_process");
|
|
3136
3323
|
var pty2 = __toESM(require("node-pty"));
|
|
3324
|
+
var log9 = createLogger("pty");
|
|
3137
3325
|
function isInteractiveShell(command) {
|
|
3138
3326
|
const trimmed = String(command).trim();
|
|
3139
3327
|
return ["bash", "sh", "zsh", "fish"].includes(trimmed);
|
|
@@ -3166,7 +3354,7 @@ async function sendInputToTmux(sessionId, data) {
|
|
|
3166
3354
|
if (!target) return;
|
|
3167
3355
|
const sessionName = target.split(":")[0].split(".")[0];
|
|
3168
3356
|
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(sessionName)) {
|
|
3169
|
-
|
|
3357
|
+
log9.warn("invalid tmux session name", { sessionName });
|
|
3170
3358
|
return;
|
|
3171
3359
|
}
|
|
3172
3360
|
try {
|
|
@@ -3178,7 +3366,7 @@ async function sendInputToTmux(sessionId, data) {
|
|
|
3178
3366
|
});
|
|
3179
3367
|
} catch (error) {
|
|
3180
3368
|
const message = error instanceof Error ? error.message : String(error);
|
|
3181
|
-
|
|
3369
|
+
log9.warn("failed to send input", { sessionId, error: message });
|
|
3182
3370
|
}
|
|
3183
3371
|
}
|
|
3184
3372
|
async function stopAllDetachedTmuxSessions() {
|
|
@@ -3206,6 +3394,7 @@ async function stopAllDetachedTmuxSessions() {
|
|
|
3206
3394
|
}
|
|
3207
3395
|
|
|
3208
3396
|
// src/shell-manager.ts
|
|
3397
|
+
var log10 = createLogger("shell");
|
|
3209
3398
|
var ShellManager = class {
|
|
3210
3399
|
ptyProcess = null;
|
|
3211
3400
|
suppressNextRespawn = false;
|
|
@@ -3246,7 +3435,7 @@ var ShellManager = class {
|
|
|
3246
3435
|
} catch (error) {
|
|
3247
3436
|
const message = error instanceof Error ? error.message : String(error);
|
|
3248
3437
|
if (!message.includes("EBADF")) {
|
|
3249
|
-
|
|
3438
|
+
log10.warn("resize failed", { error: message });
|
|
3250
3439
|
}
|
|
3251
3440
|
}
|
|
3252
3441
|
}
|
|
@@ -3262,7 +3451,7 @@ var ShellManager = class {
|
|
|
3262
3451
|
return;
|
|
3263
3452
|
}
|
|
3264
3453
|
if (!this.shouldRun) return;
|
|
3265
|
-
|
|
3454
|
+
log10.warn("shell exited; restarting shell process");
|
|
3266
3455
|
setTimeout(() => {
|
|
3267
3456
|
if (this.shouldRun) this.spawnOrRespawn();
|
|
3268
3457
|
}, 200);
|
|
@@ -3368,6 +3557,7 @@ var InventoryManager = class {
|
|
|
3368
3557
|
|
|
3369
3558
|
// src/connection-manager.ts
|
|
3370
3559
|
var import_ws3 = __toESM(require("ws"));
|
|
3560
|
+
var log11 = createLogger("connection");
|
|
3371
3561
|
var OPPORTUNISTIC_INVENTORY_SYNC_MS = 1200;
|
|
3372
3562
|
var MIN_INVENTORY_SYNC_GAP_MS = 1500;
|
|
3373
3563
|
var ConnectionManager = class {
|
|
@@ -3375,6 +3565,31 @@ var ConnectionManager = class {
|
|
|
3375
3565
|
activeGroups = { privateGroup: "", registryGroup: "" };
|
|
3376
3566
|
sessionKey = null;
|
|
3377
3567
|
reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
|
|
3568
|
+
/** Outbound frame counter; reset on every new handshake. See #310. */
|
|
3569
|
+
txSeq = new SequenceTracker();
|
|
3570
|
+
/**
|
|
3571
|
+
* Inbound frame trackers, keyed by viewerId. The portal's Terminal.tsx
|
|
3572
|
+
* initializes a fresh txSeq (starting at 1) on every component mount, so a
|
|
3573
|
+
* browser refresh — or a second viewer joining — cannot reuse the tracker
|
|
3574
|
+
* state of a prior session without triggering replay rejection. See #392.
|
|
3575
|
+
* A reserved `_legacy` key catches frames from pre-viewerId clients.
|
|
3576
|
+
*/
|
|
3577
|
+
rxSeqByViewer = /* @__PURE__ */ new Map();
|
|
3578
|
+
/**
|
|
3579
|
+
* Fetch (or lazily create) the inbound sequence tracker for `viewerId`.
|
|
3580
|
+
* Untrusted input: an empty or missing viewerId falls back to `_legacy`,
|
|
3581
|
+
* which preserves compatibility with older portal builds that didn't send
|
|
3582
|
+
* the field.
|
|
3583
|
+
*/
|
|
3584
|
+
getRxSeqForViewer(viewerId) {
|
|
3585
|
+
const key = typeof viewerId === "string" && viewerId.length > 0 ? viewerId : "_legacy";
|
|
3586
|
+
let tracker = this.rxSeqByViewer.get(key);
|
|
3587
|
+
if (!tracker) {
|
|
3588
|
+
tracker = new SequenceTracker();
|
|
3589
|
+
this.rxSeqByViewer.set(key, tracker);
|
|
3590
|
+
}
|
|
3591
|
+
return tracker;
|
|
3592
|
+
}
|
|
3378
3593
|
wsHeartbeatTimer = null;
|
|
3379
3594
|
inventorySyncTimer = null;
|
|
3380
3595
|
inventorySyncKickTimer = null;
|
|
@@ -3399,6 +3614,10 @@ var ConnectionManager = class {
|
|
|
3399
3614
|
setConnection(ws, groups, sessionKey) {
|
|
3400
3615
|
this.activeSocket = ws;
|
|
3401
3616
|
this.activeGroups = groups;
|
|
3617
|
+
if (sessionKey !== this.sessionKey) {
|
|
3618
|
+
this.txSeq.reset();
|
|
3619
|
+
this.rxSeqByViewer.clear();
|
|
3620
|
+
}
|
|
3402
3621
|
this.sessionKey = sessionKey;
|
|
3403
3622
|
}
|
|
3404
3623
|
/** Reset reconnect delay on successful connection. */
|
|
@@ -3421,7 +3640,7 @@ var ConnectionManager = class {
|
|
|
3421
3640
|
if (!this.activeSocket || this.activeSocket.readyState !== import_ws3.default.OPEN) return;
|
|
3422
3641
|
this.activeSocket.ping();
|
|
3423
3642
|
this.wsPongTimeout = setTimeout(() => {
|
|
3424
|
-
|
|
3643
|
+
log11.warn("no pong received within 10s; closing stale connection");
|
|
3425
3644
|
try {
|
|
3426
3645
|
this.activeSocket?.terminate();
|
|
3427
3646
|
} catch {
|
|
@@ -3510,9 +3729,10 @@ var ConnectionManager = class {
|
|
|
3510
3729
|
replayBufferedOutput(sendChunk) {
|
|
3511
3730
|
const buffered = this.outputBuffer.drain();
|
|
3512
3731
|
if (buffered.length > 0) {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3732
|
+
log11.info("replaying buffered output chunks", {
|
|
3733
|
+
chunks: buffered.length,
|
|
3734
|
+
bytes: buffered.reduce((sum, c) => sum + Buffer.byteLength(c, "utf8"), 0)
|
|
3735
|
+
});
|
|
3516
3736
|
for (const chunk of buffered) {
|
|
3517
3737
|
sendChunk(chunk);
|
|
3518
3738
|
}
|
|
@@ -3548,6 +3768,7 @@ var import_child_process3 = require("child_process");
|
|
|
3548
3768
|
var fs4 = __toESM(require("fs"));
|
|
3549
3769
|
var os7 = __toESM(require("os"));
|
|
3550
3770
|
var path3 = __toESM(require("path"));
|
|
3771
|
+
var log12 = createLogger("service");
|
|
3551
3772
|
function assertConfiguredAgentToken() {
|
|
3552
3773
|
const config = loadConfig();
|
|
3553
3774
|
if (!config.agentToken) {
|
|
@@ -3613,7 +3834,7 @@ async function installSystemdService(opts = {}) {
|
|
|
3613
3834
|
await runSystemctlUser(["restart", SERVICE_NAME]);
|
|
3614
3835
|
}
|
|
3615
3836
|
saveConfigPatch({ serviceBackend: "systemd" });
|
|
3616
|
-
|
|
3837
|
+
log12.info("installed systemd user service", { path: SERVICE_FILE });
|
|
3617
3838
|
}
|
|
3618
3839
|
function readFallbackPid() {
|
|
3619
3840
|
try {
|
|
@@ -3652,7 +3873,7 @@ async function startPidfileService() {
|
|
|
3652
3873
|
assertConfiguredAgentToken();
|
|
3653
3874
|
const existingPid = readFallbackPid();
|
|
3654
3875
|
if (existingPid && isProcessRunning(existingPid)) {
|
|
3655
|
-
|
|
3876
|
+
log12.info("service already running", { pid: existingPid });
|
|
3656
3877
|
return;
|
|
3657
3878
|
}
|
|
3658
3879
|
ensureConfigDir();
|
|
@@ -3669,8 +3890,8 @@ async function startPidfileService() {
|
|
|
3669
3890
|
fs4.writeFileSync(FALLBACK_PID_FILE, `${child.pid}
|
|
3670
3891
|
`, "utf8");
|
|
3671
3892
|
saveConfigPatch({ serviceBackend: "pidfile" });
|
|
3672
|
-
|
|
3673
|
-
|
|
3893
|
+
log12.info("started fallback background service", { pid: child.pid });
|
|
3894
|
+
log12.info("logs", { path: FALLBACK_LOG_FILE });
|
|
3674
3895
|
}
|
|
3675
3896
|
async function stopPidfileService() {
|
|
3676
3897
|
const pid = readFallbackPid();
|
|
@@ -3679,7 +3900,7 @@ async function stopPidfileService() {
|
|
|
3679
3900
|
fs4.unlinkSync(FALLBACK_PID_FILE);
|
|
3680
3901
|
} catch {
|
|
3681
3902
|
}
|
|
3682
|
-
|
|
3903
|
+
log12.info("service is not running");
|
|
3683
3904
|
return;
|
|
3684
3905
|
}
|
|
3685
3906
|
process.kill(pid, "SIGTERM");
|
|
@@ -3694,7 +3915,7 @@ async function stopPidfileService() {
|
|
|
3694
3915
|
fs4.unlinkSync(FALLBACK_PID_FILE);
|
|
3695
3916
|
} catch {
|
|
3696
3917
|
}
|
|
3697
|
-
|
|
3918
|
+
log12.info("stopped fallback background service", { pid });
|
|
3698
3919
|
}
|
|
3699
3920
|
async function ensureServiceInstalled(opts = {}) {
|
|
3700
3921
|
const wantsSystemd = await canUseSystemdUser();
|
|
@@ -3706,9 +3927,7 @@ async function ensureServiceInstalled(opts = {}) {
|
|
|
3706
3927
|
if (opts.start) {
|
|
3707
3928
|
await startPidfileService();
|
|
3708
3929
|
} else {
|
|
3709
|
-
|
|
3710
|
-
"[rAgent] systemd user manager unavailable; using fallback pidfile backend."
|
|
3711
|
-
);
|
|
3930
|
+
log12.info("systemd user manager unavailable; using fallback pidfile backend");
|
|
3712
3931
|
}
|
|
3713
3932
|
return "pidfile";
|
|
3714
3933
|
}
|
|
@@ -3716,7 +3935,7 @@ async function startService() {
|
|
|
3716
3935
|
const backend = getConfiguredServiceBackend();
|
|
3717
3936
|
if (backend === "systemd") {
|
|
3718
3937
|
await runSystemctlUser(["start", SERVICE_NAME]);
|
|
3719
|
-
|
|
3938
|
+
log12.info("started service via systemd");
|
|
3720
3939
|
return;
|
|
3721
3940
|
}
|
|
3722
3941
|
if (backend === "pidfile") {
|
|
@@ -3729,7 +3948,7 @@ async function stopService() {
|
|
|
3729
3948
|
const backend = getConfiguredServiceBackend();
|
|
3730
3949
|
if (backend === "systemd") {
|
|
3731
3950
|
await runSystemctlUser(["stop", SERVICE_NAME]);
|
|
3732
|
-
|
|
3951
|
+
log12.info("stopped service via systemd");
|
|
3733
3952
|
return;
|
|
3734
3953
|
}
|
|
3735
3954
|
await stopPidfileService();
|
|
@@ -3747,7 +3966,7 @@ function killStaleAgentProcesses() {
|
|
|
3747
3966
|
try {
|
|
3748
3967
|
process.kill(pid, 0);
|
|
3749
3968
|
process.kill(pid, "SIGTERM");
|
|
3750
|
-
|
|
3969
|
+
log12.info("stopped stale agent process", { pid });
|
|
3751
3970
|
} catch {
|
|
3752
3971
|
}
|
|
3753
3972
|
fs4.unlinkSync(pidPath);
|
|
@@ -3762,7 +3981,7 @@ async function restartService() {
|
|
|
3762
3981
|
killStaleAgentProcesses();
|
|
3763
3982
|
if (backend === "systemd") {
|
|
3764
3983
|
await runSystemctlUser(["restart", SERVICE_NAME]);
|
|
3765
|
-
|
|
3984
|
+
log12.info("restarted service via systemd");
|
|
3766
3985
|
return;
|
|
3767
3986
|
}
|
|
3768
3987
|
await stopPidfileService();
|
|
@@ -3778,17 +3997,17 @@ async function printServiceStatus() {
|
|
|
3778
3997
|
if (status) {
|
|
3779
3998
|
process.stdout.write(status);
|
|
3780
3999
|
} else {
|
|
3781
|
-
|
|
4000
|
+
log12.info("systemd service is not installed or has no status");
|
|
3782
4001
|
}
|
|
3783
4002
|
return;
|
|
3784
4003
|
}
|
|
3785
4004
|
const pid = readFallbackPid();
|
|
3786
4005
|
if (pid && isProcessRunning(pid)) {
|
|
3787
|
-
|
|
3788
|
-
|
|
4006
|
+
log12.info("fallback service running", { pid });
|
|
4007
|
+
log12.info("logs", { path: FALLBACK_LOG_FILE });
|
|
3789
4008
|
return;
|
|
3790
4009
|
}
|
|
3791
|
-
|
|
4010
|
+
log12.info("service is not running");
|
|
3792
4011
|
}
|
|
3793
4012
|
async function printServiceLogs(opts) {
|
|
3794
4013
|
const lines = Number.parseInt(String(opts.lines || 100), 10) || 100;
|
|
@@ -3814,7 +4033,7 @@ async function printServiceLogs(opts) {
|
|
|
3814
4033
|
return;
|
|
3815
4034
|
}
|
|
3816
4035
|
if (!fs4.existsSync(FALLBACK_LOG_FILE)) {
|
|
3817
|
-
|
|
4036
|
+
log12.info("no log file found", { path: FALLBACK_LOG_FILE });
|
|
3818
4037
|
return;
|
|
3819
4038
|
}
|
|
3820
4039
|
if (follow) {
|
|
@@ -3846,7 +4065,7 @@ async function uninstallService() {
|
|
|
3846
4065
|
const config = loadConfig();
|
|
3847
4066
|
delete config.serviceBackend;
|
|
3848
4067
|
saveConfig(config);
|
|
3849
|
-
|
|
4068
|
+
log12.info("service uninstalled");
|
|
3850
4069
|
}
|
|
3851
4070
|
function requestStopSelfService() {
|
|
3852
4071
|
const backend = getConfiguredServiceBackend();
|
|
@@ -4071,7 +4290,105 @@ async function executeProvision(request, onProgress) {
|
|
|
4071
4290
|
return startAgent(request, emit);
|
|
4072
4291
|
}
|
|
4073
4292
|
|
|
4293
|
+
// src/command-guard.ts
|
|
4294
|
+
var import_node_path2 = require("path");
|
|
4295
|
+
var posixNormalize = import_node_path2.posix.normalize;
|
|
4296
|
+
var PATTERNS = [
|
|
4297
|
+
// Any command/expression substitution. Legitimate agent launchers never
|
|
4298
|
+
// need these — forbidding them blocks smuggled commands like
|
|
4299
|
+
// `claude $(rm -rf /)` or `aider `rm -rf /``.
|
|
4300
|
+
{
|
|
4301
|
+
category: "subshell_expansion",
|
|
4302
|
+
re: /\$\(|`/,
|
|
4303
|
+
pattern: "subshell expansion $(...) or backticks"
|
|
4304
|
+
},
|
|
4305
|
+
// Download-and-pipe-to-shell: `curl … | sh`, `wget … | bash`, etc.
|
|
4306
|
+
{
|
|
4307
|
+
category: "pipe_to_shell",
|
|
4308
|
+
re: /\b(?:curl|wget|fetch|lwp-request)\b[^\n]{0,400}?\|[^\n]{0,100}?\b(?:sh|bash|zsh|fish|dash|ksh|csh|tcsh|ash)\b/i,
|
|
4309
|
+
pattern: "curl/wget piped into a shell"
|
|
4310
|
+
},
|
|
4311
|
+
// base64 → decode → shell.
|
|
4312
|
+
{
|
|
4313
|
+
category: "base64_pipe",
|
|
4314
|
+
re: /\bbase64\b[^\n]{0,200}?(?:-d|--decode)\b[^\n]{0,400}?\|[^\n]{0,100}?\b(?:sh|bash|zsh|fish|dash)\b/i,
|
|
4315
|
+
pattern: "base64 decode piped into a shell"
|
|
4316
|
+
},
|
|
4317
|
+
// `rm -rf /`, `rm -r ~`, `rm -rf *`, `rm -fr $HOME`, etc.
|
|
4318
|
+
// Matches -r, -R, -f, -e in any order/combination.
|
|
4319
|
+
{
|
|
4320
|
+
category: "rm_dangerous_target",
|
|
4321
|
+
re: /\brm\b(?:\s+-[a-zA-Z]*[rRfe][a-zA-Z]*){1,4}\s+(?:\/(?:\s|$)|~(?:\s|$|\/)|\$HOME\b|\*\s*$|\*\s+|\.{1,2}\s*$)/i,
|
|
4322
|
+
pattern: "rm against /, ~, $HOME, *, or ."
|
|
4323
|
+
},
|
|
4324
|
+
// mkfs.ext4, mkfs.btrfs, plain mkfs, etc.
|
|
4325
|
+
{
|
|
4326
|
+
category: "mkfs",
|
|
4327
|
+
re: /\bmkfs(?:\.[a-z0-9]+)?\b/i,
|
|
4328
|
+
pattern: "mkfs* filesystem format"
|
|
4329
|
+
},
|
|
4330
|
+
// dd of=/dev/sda, dd of=/dev/nvme0n1, etc.
|
|
4331
|
+
{
|
|
4332
|
+
category: "dd_write_device",
|
|
4333
|
+
re: /\bdd\b[^\n|&;]{0,300}?\bof=\/dev\/[a-z]/i,
|
|
4334
|
+
pattern: "dd writing to a block device"
|
|
4335
|
+
},
|
|
4336
|
+
// Shell redirects writing to block devices.
|
|
4337
|
+
{
|
|
4338
|
+
category: "write_device",
|
|
4339
|
+
re: />\s*\/dev\/(?:sd|nvme|hd|mmc|xvd|disk|loop)/i,
|
|
4340
|
+
pattern: "redirect to a block device"
|
|
4341
|
+
},
|
|
4342
|
+
// Classic fork bomb :(){ :|: & };:
|
|
4343
|
+
// Whitespace-tolerant; covers `:() { :|: & };:` variants.
|
|
4344
|
+
{
|
|
4345
|
+
category: "fork_bomb",
|
|
4346
|
+
re: /:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:\s*&\s*;?\s*\}\s*;?\s*:/,
|
|
4347
|
+
pattern: "fork bomb definition"
|
|
4348
|
+
},
|
|
4349
|
+
// chmod -R 7xx / (world-writable on root).
|
|
4350
|
+
{
|
|
4351
|
+
category: "chmod_permissive_root",
|
|
4352
|
+
re: /\bchmod\b(?:\s+-[rR]+)?\s+[0-7]*7[0-7]*\s+\/(?:\s|$)/i,
|
|
4353
|
+
pattern: "chmod with permissive perms on /"
|
|
4354
|
+
},
|
|
4355
|
+
// chown … /
|
|
4356
|
+
{
|
|
4357
|
+
category: "chown_root",
|
|
4358
|
+
re: /\bchown\b(?:\s+-[rR]+)?\s+\S+\s+\/(?:\s|$)/i,
|
|
4359
|
+
pattern: "chown targeting /"
|
|
4360
|
+
},
|
|
4361
|
+
// ln -s … /dev/… — creating symlinks into /dev lets later redirects hit
|
|
4362
|
+
// device nodes through a benign-looking relative path.
|
|
4363
|
+
{
|
|
4364
|
+
category: "symlink_to_device",
|
|
4365
|
+
re: /\bln\b\s+-[^\s]*s[^\s]*\s+\/dev\//i,
|
|
4366
|
+
pattern: "ln -s into /dev"
|
|
4367
|
+
},
|
|
4368
|
+
// Host-level power actions.
|
|
4369
|
+
{
|
|
4370
|
+
category: "shutdown",
|
|
4371
|
+
re: /\b(?:shutdown|reboot|halt|poweroff|kexec)\b/i,
|
|
4372
|
+
pattern: "shutdown/reboot/halt/poweroff/kexec"
|
|
4373
|
+
}
|
|
4374
|
+
];
|
|
4375
|
+
function detectDangerousCommand(cmd) {
|
|
4376
|
+
if (typeof cmd !== "string" || cmd.length === 0) return null;
|
|
4377
|
+
for (const { category, re, pattern } of PATTERNS) {
|
|
4378
|
+
if (re.test(cmd)) return { category, pattern };
|
|
4379
|
+
}
|
|
4380
|
+
return null;
|
|
4381
|
+
}
|
|
4382
|
+
function isSafeWorkingDir(raw) {
|
|
4383
|
+
if (typeof raw !== "string" || raw.length === 0) return false;
|
|
4384
|
+
if (!raw.startsWith("/")) return false;
|
|
4385
|
+
const normalized = posixNormalize(raw);
|
|
4386
|
+
if (normalized === "/dev" || normalized.startsWith("/dev/")) return false;
|
|
4387
|
+
return true;
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4074
4390
|
// src/control-dispatcher.ts
|
|
4391
|
+
var log13 = createLogger("control");
|
|
4075
4392
|
function collectProcessTreePids(rootPid, visited = /* @__PURE__ */ new Set()) {
|
|
4076
4393
|
if (visited.has(rootPid)) return [];
|
|
4077
4394
|
visited.add(rootPid);
|
|
@@ -4153,11 +4470,11 @@ var ControlDispatcher = class {
|
|
|
4153
4470
|
if (!sessionKey) return true;
|
|
4154
4471
|
const receivedHmac = payload.hmac;
|
|
4155
4472
|
if (typeof receivedHmac !== "string") {
|
|
4156
|
-
|
|
4473
|
+
log13.warn("control message missing HMAC signature");
|
|
4157
4474
|
return false;
|
|
4158
4475
|
}
|
|
4159
4476
|
if (!/^[0-9a-f]{64}$/i.test(receivedHmac)) {
|
|
4160
|
-
|
|
4477
|
+
log13.warn("control message has malformed HMAC signature");
|
|
4161
4478
|
return false;
|
|
4162
4479
|
}
|
|
4163
4480
|
const { hmac: _, ...payloadWithoutHmac } = payload;
|
|
@@ -4183,7 +4500,7 @@ var ControlDispatcher = class {
|
|
|
4183
4500
|
]);
|
|
4184
4501
|
if (dangerousActions.has(action) && this.connection.sessionKey) {
|
|
4185
4502
|
if (!this.verifyMessageHmac(payload)) {
|
|
4186
|
-
|
|
4503
|
+
log13.warn("rejecting control action \u2014 HMAC verification failed", { action });
|
|
4187
4504
|
return;
|
|
4188
4505
|
}
|
|
4189
4506
|
}
|
|
@@ -4219,10 +4536,10 @@ var ControlDispatcher = class {
|
|
|
4219
4536
|
this.transcriptWatcher?.disableMarkdown(sessionId);
|
|
4220
4537
|
try {
|
|
4221
4538
|
await stopTmuxPaneBySessionId(sessionId);
|
|
4222
|
-
|
|
4539
|
+
log13.info("closed remote session", { sessionId });
|
|
4223
4540
|
} catch (error) {
|
|
4224
4541
|
const message = error instanceof Error ? error.message : String(error);
|
|
4225
|
-
|
|
4542
|
+
log13.warn("failed to close session", { sessionId, error: message });
|
|
4226
4543
|
}
|
|
4227
4544
|
await delay(300);
|
|
4228
4545
|
await this.syncInventory(true);
|
|
@@ -4233,7 +4550,7 @@ var ControlDispatcher = class {
|
|
|
4233
4550
|
{
|
|
4234
4551
|
const pid = parseProcessPid2(sessionId);
|
|
4235
4552
|
if (pid === null) {
|
|
4236
|
-
|
|
4553
|
+
log13.warn("kill-process: could not parse PID", { sessionId });
|
|
4237
4554
|
return;
|
|
4238
4555
|
}
|
|
4239
4556
|
this.transcriptWatcher?.disableMarkdown(sessionId);
|
|
@@ -4249,11 +4566,11 @@ var ControlDispatcher = class {
|
|
|
4249
4566
|
const code = error.code;
|
|
4250
4567
|
if (code !== "ESRCH") {
|
|
4251
4568
|
const message = error instanceof Error ? error.message : String(error);
|
|
4252
|
-
|
|
4569
|
+
log13.warn("failed to send SIGTERM", { pid: targetPid, error: message });
|
|
4253
4570
|
}
|
|
4254
4571
|
}
|
|
4255
4572
|
}
|
|
4256
|
-
|
|
4573
|
+
log13.info("sent SIGTERM to processes", { signaled, sessionId });
|
|
4257
4574
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
4258
4575
|
for (const targetPid of processTree) {
|
|
4259
4576
|
if (!isProcessAlive(targetPid)) continue;
|
|
@@ -4263,13 +4580,13 @@ var ControlDispatcher = class {
|
|
|
4263
4580
|
const code = error.code;
|
|
4264
4581
|
if (code !== "ESRCH") {
|
|
4265
4582
|
const message = error instanceof Error ? error.message : String(error);
|
|
4266
|
-
|
|
4583
|
+
log13.warn("failed to send SIGKILL", { pid: targetPid, error: message });
|
|
4267
4584
|
}
|
|
4268
4585
|
}
|
|
4269
4586
|
}
|
|
4270
4587
|
} catch (error) {
|
|
4271
4588
|
const message = error instanceof Error ? error.message : String(error);
|
|
4272
|
-
|
|
4589
|
+
log13.warn("failed to kill session", { sessionId, error: message });
|
|
4273
4590
|
}
|
|
4274
4591
|
await this.syncInventory(true);
|
|
4275
4592
|
}
|
|
@@ -4284,7 +4601,7 @@ var ControlDispatcher = class {
|
|
|
4284
4601
|
}
|
|
4285
4602
|
}
|
|
4286
4603
|
const killed = await stopAllDetachedTmuxSessions();
|
|
4287
|
-
|
|
4604
|
+
log13.info("killed detached tmux sessions", { count: killed });
|
|
4288
4605
|
await delay(300);
|
|
4289
4606
|
await this.syncInventory(true);
|
|
4290
4607
|
return;
|
|
@@ -4340,10 +4657,10 @@ var ControlDispatcher = class {
|
|
|
4340
4657
|
async handleProvision(payload) {
|
|
4341
4658
|
const provReq = validateProvisionRequest(payload);
|
|
4342
4659
|
if (!provReq) {
|
|
4343
|
-
|
|
4660
|
+
log13.warn("rejecting provision request \u2014 malformed payload");
|
|
4344
4661
|
return;
|
|
4345
4662
|
}
|
|
4346
|
-
|
|
4663
|
+
log13.info("provision request", { name: provReq.manifest.name, provisionId: provReq.provisionId });
|
|
4347
4664
|
const sendProgress = (progress) => {
|
|
4348
4665
|
const ws = this.connection.activeSocket;
|
|
4349
4666
|
if (ws && ws.readyState === import_ws4.default.OPEN && this.connection.activeGroups.registryGroup) {
|
|
@@ -4380,7 +4697,7 @@ var ControlDispatcher = class {
|
|
|
4380
4697
|
status: "unavailable",
|
|
4381
4698
|
reason: result.reason
|
|
4382
4699
|
});
|
|
4383
|
-
|
|
4700
|
+
log13.info("could not enable markdown", { sessionId, reason: result.reason });
|
|
4384
4701
|
} else {
|
|
4385
4702
|
this.sendMarkdownStatus({
|
|
4386
4703
|
type: "markdown",
|
|
@@ -4397,7 +4714,7 @@ var ControlDispatcher = class {
|
|
|
4397
4714
|
const ws = this.connection.activeSocket;
|
|
4398
4715
|
const group = this.connection.activeGroups.privateGroup;
|
|
4399
4716
|
if (!ws || ws.readyState !== import_ws4.default.OPEN || !group) return;
|
|
4400
|
-
sendToGroup(ws, group, payload);
|
|
4717
|
+
sendToGroup(ws, group, { ...payload });
|
|
4401
4718
|
}
|
|
4402
4719
|
/** Stop all transcript watchers (called on disconnect/cleanup). */
|
|
4403
4720
|
stopTranscriptWatchers() {
|
|
@@ -4414,19 +4731,59 @@ var ControlDispatcher = class {
|
|
|
4414
4731
|
const sessionName = typeof payload?.sessionName === "string" && payload.sessionName.trim().length > 0 ? payload.sessionName.trim() : `agent-${Date.now().toString(36)}`;
|
|
4415
4732
|
const cmd = typeof payload?.command === "string" && payload.command.trim().length > 0 ? payload.command.trim() : null;
|
|
4416
4733
|
if (!cmd) {
|
|
4417
|
-
|
|
4734
|
+
log13.warn("start-agent: no command provided, ignoring");
|
|
4418
4735
|
return;
|
|
4419
4736
|
}
|
|
4420
4737
|
if (sessionName.length > 128 || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
|
|
4421
|
-
|
|
4738
|
+
log13.warn("start-agent: invalid session name, ignoring");
|
|
4422
4739
|
return;
|
|
4423
4740
|
}
|
|
4424
|
-
const
|
|
4425
|
-
if (
|
|
4426
|
-
|
|
4741
|
+
const danger = detectDangerousCommand(cmd);
|
|
4742
|
+
if (danger) {
|
|
4743
|
+
if (!this._approvalEnforcer) {
|
|
4744
|
+
log13.warn("start-agent: rejected dangerous command (no approval enforcer)", {
|
|
4745
|
+
category: danger.category,
|
|
4746
|
+
pattern: danger.pattern,
|
|
4747
|
+
command: cmd
|
|
4748
|
+
});
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
log13.info("start-agent: dangerous command \u2014 awaiting approval", {
|
|
4752
|
+
category: danger.category,
|
|
4753
|
+
pattern: danger.pattern,
|
|
4754
|
+
sessionName
|
|
4755
|
+
});
|
|
4756
|
+
const response = await this._approvalEnforcer.checkCommand(
|
|
4757
|
+
cmd,
|
|
4758
|
+
this.options.hostId,
|
|
4759
|
+
sessionName
|
|
4760
|
+
);
|
|
4761
|
+
if (response === null) {
|
|
4762
|
+
log13.warn("start-agent: dangerous command not covered by approval policy, rejecting", {
|
|
4763
|
+
category: danger.category,
|
|
4764
|
+
command: cmd
|
|
4765
|
+
});
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4768
|
+
if (response.type !== "approved") {
|
|
4769
|
+
log13.warn("start-agent: dangerous command denied or timed out", {
|
|
4770
|
+
category: danger.category,
|
|
4771
|
+
sessionName,
|
|
4772
|
+
responseType: response.type
|
|
4773
|
+
});
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
log13.info("start-agent: dangerous command approved", {
|
|
4777
|
+
category: danger.category,
|
|
4778
|
+
sessionName
|
|
4779
|
+
});
|
|
4780
|
+
}
|
|
4781
|
+
const rawWorkingDir = typeof payload?.workingDir === "string" && payload.workingDir.trim().length > 0 ? payload.workingDir.trim() : void 0;
|
|
4782
|
+
if (rawWorkingDir !== void 0 && !isSafeWorkingDir(rawWorkingDir)) {
|
|
4783
|
+
log13.warn("start-agent: rejected unsafe workingDir", { workingDir: rawWorkingDir });
|
|
4427
4784
|
return;
|
|
4428
4785
|
}
|
|
4429
|
-
const workingDir =
|
|
4786
|
+
const workingDir = rawWorkingDir;
|
|
4430
4787
|
const envVars = payload?.envVars && typeof payload.envVars === "object" ? payload.envVars : void 0;
|
|
4431
4788
|
const tmuxArgs = ["new-session", "-d", "-s", sessionName];
|
|
4432
4789
|
if (workingDir) {
|
|
@@ -4440,10 +4797,10 @@ var ControlDispatcher = class {
|
|
|
4440
4797
|
tmuxArgs.push(fullCmd);
|
|
4441
4798
|
try {
|
|
4442
4799
|
(0, import_child_process5.execFileSync)("tmux", tmuxArgs, { stdio: "ignore" });
|
|
4443
|
-
|
|
4800
|
+
log13.info("started agent session", { sessionName, command: cmd });
|
|
4444
4801
|
} catch (error) {
|
|
4445
4802
|
const message = error instanceof Error ? error.message : String(error);
|
|
4446
|
-
|
|
4803
|
+
log13.error("failed to start agent session", { sessionName, error: message });
|
|
4447
4804
|
}
|
|
4448
4805
|
await this.syncInventory(true);
|
|
4449
4806
|
}
|
|
@@ -4706,6 +5063,7 @@ function isGrantExpired(grant) {
|
|
|
4706
5063
|
}
|
|
4707
5064
|
|
|
4708
5065
|
// src/approval-enforcer.ts
|
|
5066
|
+
var log14 = createLogger("approval");
|
|
4709
5067
|
var ApprovalEnforcer = class {
|
|
4710
5068
|
policy;
|
|
4711
5069
|
secret;
|
|
@@ -4729,16 +5087,20 @@ var ApprovalEnforcer = class {
|
|
|
4729
5087
|
async checkCommand(command, hostId, sessionId) {
|
|
4730
5088
|
const request = matchPolicy(command, this.policy, hostId, sessionId);
|
|
4731
5089
|
if (!request) return null;
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
5090
|
+
log14.info("approval pending", {
|
|
5091
|
+
action: request.action,
|
|
5092
|
+
severity: request.severity,
|
|
5093
|
+
requestId: request.requestId
|
|
5094
|
+
});
|
|
4735
5095
|
return new Promise((resolve) => {
|
|
4736
5096
|
const timer = setTimeout(() => {
|
|
4737
5097
|
this.pendingRequests.delete(request.requestId);
|
|
4738
5098
|
const response = { type: "timeout" };
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
5099
|
+
log14.info("approval timeout", {
|
|
5100
|
+
action: request.action,
|
|
5101
|
+
severity: request.severity,
|
|
5102
|
+
requestId: request.requestId
|
|
5103
|
+
});
|
|
4742
5104
|
resolve(response);
|
|
4743
5105
|
}, request.timeoutMs);
|
|
4744
5106
|
this.pendingRequests.set(request.requestId, { request, resolve, timer });
|
|
@@ -4754,29 +5116,33 @@ var ApprovalEnforcer = class {
|
|
|
4754
5116
|
if (!requestId) return;
|
|
4755
5117
|
const pending = this.pendingRequests.get(requestId);
|
|
4756
5118
|
if (!pending) {
|
|
4757
|
-
|
|
4758
|
-
`[rAgent] Approval: received response for unknown request [${requestId}]`
|
|
4759
|
-
);
|
|
5119
|
+
log14.warn("received response for unknown approval request", { requestId });
|
|
4760
5120
|
return;
|
|
4761
5121
|
}
|
|
4762
5122
|
if (response.type === "approved") {
|
|
4763
5123
|
if (!this.acceptGrant(response.grant)) {
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
5124
|
+
log14.info("approval rejected (invalid grant)", {
|
|
5125
|
+
action: pending.request.action,
|
|
5126
|
+
severity: pending.request.severity,
|
|
5127
|
+
requestId
|
|
5128
|
+
});
|
|
4767
5129
|
clearTimeout(pending.timer);
|
|
4768
5130
|
this.pendingRequests.delete(requestId);
|
|
4769
5131
|
pending.resolve({ type: "denied", denial: { requestId, deniedBy: "system", deniedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "Invalid or expired grant" } });
|
|
4770
5132
|
return;
|
|
4771
5133
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
5134
|
+
log14.info("approval approved", {
|
|
5135
|
+
action: pending.request.action,
|
|
5136
|
+
severity: pending.request.severity,
|
|
5137
|
+
requestId
|
|
5138
|
+
});
|
|
4775
5139
|
} else if (response.type === "denied") {
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
5140
|
+
log14.info("approval denied", {
|
|
5141
|
+
action: pending.request.action,
|
|
5142
|
+
severity: pending.request.severity,
|
|
5143
|
+
requestId,
|
|
5144
|
+
reason: response.denial.reason ?? ""
|
|
5145
|
+
});
|
|
4780
5146
|
}
|
|
4781
5147
|
clearTimeout(pending.timer);
|
|
4782
5148
|
this.pendingRequests.delete(requestId);
|
|
@@ -4812,9 +5178,11 @@ var ApprovalEnforcer = class {
|
|
|
4812
5178
|
for (const [id, entry] of this.pendingRequests) {
|
|
4813
5179
|
clearTimeout(entry.timer);
|
|
4814
5180
|
this.pendingRequests.delete(id);
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
5181
|
+
log14.info("approval stopped", {
|
|
5182
|
+
action: entry.request.action,
|
|
5183
|
+
severity: entry.request.severity,
|
|
5184
|
+
requestId: id
|
|
5185
|
+
});
|
|
4818
5186
|
entry.resolve({ type: "timeout" });
|
|
4819
5187
|
}
|
|
4820
5188
|
}
|
|
@@ -4839,6 +5207,7 @@ var ApprovalEnforcer = class {
|
|
|
4839
5207
|
};
|
|
4840
5208
|
|
|
4841
5209
|
// src/agent.ts
|
|
5210
|
+
var log15 = createLogger("agent");
|
|
4842
5211
|
var MAX_TRANSPORT_CHUNK_BYTES = 24 * 1024;
|
|
4843
5212
|
function pidFilePath(hostId) {
|
|
4844
5213
|
return path4.join(CONFIG_DIR, `agent-${hostId}.pid`);
|
|
@@ -4923,15 +5292,15 @@ async function runAgent(rawOptions) {
|
|
|
4923
5292
|
const config = loadConfig();
|
|
4924
5293
|
if (config.redaction?.enabled === false || rawOptions.redact === false) {
|
|
4925
5294
|
setRedactionEnabled(false);
|
|
4926
|
-
|
|
5295
|
+
log15.info("secret redaction disabled");
|
|
4927
5296
|
}
|
|
4928
|
-
|
|
4929
|
-
|
|
5297
|
+
log15.info("connector started", { hostName: options.hostName, hostId: options.hostId });
|
|
5298
|
+
log15.info("portal", { portal: options.portal });
|
|
4930
5299
|
try {
|
|
4931
5300
|
const existingSessions = await collectSessionInventory(options.hostId, options.command);
|
|
4932
5301
|
const tmuxCount = existingSessions.filter((s) => s.type === "tmux").length;
|
|
4933
5302
|
if (tmuxCount > 0) {
|
|
4934
|
-
|
|
5303
|
+
log15.info("found existing tmux sessions", { count: tmuxCount });
|
|
4935
5304
|
}
|
|
4936
5305
|
} catch {
|
|
4937
5306
|
}
|
|
@@ -4944,16 +5313,26 @@ async function runAgent(rawOptions) {
|
|
|
4944
5313
|
sessionStreamer.bufferOutput(sessionId, data);
|
|
4945
5314
|
return;
|
|
4946
5315
|
}
|
|
4947
|
-
|
|
5316
|
+
const redactedData = redactStreamingChunk(sessionId, data);
|
|
5317
|
+
for (const chunk of splitTransportChunks(redactedData)) {
|
|
4948
5318
|
if (conn.sessionKey) {
|
|
4949
|
-
const { enc, iv } = encryptPayload(chunk, conn.sessionKey);
|
|
4950
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId });
|
|
5319
|
+
const { enc, iv, seq } = encryptPayload(chunk, conn.sessionKey, conn.txSeq.next());
|
|
5320
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
|
|
4951
5321
|
} else {
|
|
4952
5322
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: chunk, sessionId });
|
|
4953
5323
|
}
|
|
4954
5324
|
}
|
|
4955
5325
|
},
|
|
4956
5326
|
(sessionId) => {
|
|
5327
|
+
const tail = flushStreamingRedaction(sessionId);
|
|
5328
|
+
if (tail && conn.isReady()) {
|
|
5329
|
+
if (conn.sessionKey) {
|
|
5330
|
+
const { enc, iv, seq } = encryptPayload(tail, conn.sessionKey, conn.txSeq.next());
|
|
5331
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId });
|
|
5332
|
+
} else {
|
|
5333
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: tail, sessionId });
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
4957
5336
|
if (!conn.isReady()) return;
|
|
4958
5337
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "stream-stopped", sessionId });
|
|
4959
5338
|
}
|
|
@@ -4965,10 +5344,11 @@ async function runAgent(rawOptions) {
|
|
|
4965
5344
|
outputBuffer.push(chunk);
|
|
4966
5345
|
return;
|
|
4967
5346
|
}
|
|
4968
|
-
|
|
5347
|
+
const redacted = redactStreamingChunk(ptySessionId, chunk);
|
|
5348
|
+
for (const outputChunk of splitTransportChunks(redacted)) {
|
|
4969
5349
|
if (conn.sessionKey) {
|
|
4970
|
-
const { enc, iv } = encryptPayload(outputChunk, conn.sessionKey);
|
|
4971
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, sessionId: ptySessionId });
|
|
5350
|
+
const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
|
|
5351
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", enc, iv, seq, sessionId: ptySessionId });
|
|
4972
5352
|
} else {
|
|
4973
5353
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "output", data: outputChunk, sessionId: ptySessionId });
|
|
4974
5354
|
}
|
|
@@ -4990,8 +5370,8 @@ async function runAgent(rawOptions) {
|
|
|
4990
5370
|
};
|
|
4991
5371
|
if (conn.sessionKey) {
|
|
4992
5372
|
const contentStr = JSON.stringify(payload);
|
|
4993
|
-
const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
|
|
4994
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
|
|
5373
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
5374
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
|
|
4995
5375
|
} else {
|
|
4996
5376
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
4997
5377
|
}
|
|
@@ -5007,8 +5387,8 @@ async function runAgent(rawOptions) {
|
|
|
5007
5387
|
};
|
|
5008
5388
|
if (conn.sessionKey) {
|
|
5009
5389
|
const contentStr = JSON.stringify(payload);
|
|
5010
|
-
const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
|
|
5011
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
|
|
5390
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
5391
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
|
|
5012
5392
|
} else {
|
|
5013
5393
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
5014
5394
|
}
|
|
@@ -5026,8 +5406,8 @@ async function runAgent(rawOptions) {
|
|
|
5026
5406
|
};
|
|
5027
5407
|
if (conn.sessionKey) {
|
|
5028
5408
|
const contentStr = JSON.stringify(payload);
|
|
5029
|
-
const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
|
|
5030
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
|
|
5409
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
5410
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
|
|
5031
5411
|
} else {
|
|
5032
5412
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
5033
5413
|
}
|
|
@@ -5045,8 +5425,8 @@ async function runAgent(rawOptions) {
|
|
|
5045
5425
|
};
|
|
5046
5426
|
if (conn.sessionKey) {
|
|
5047
5427
|
const contentStr = JSON.stringify(payload);
|
|
5048
|
-
const { enc, iv } = encryptPayload(contentStr, conn.sessionKey);
|
|
5049
|
-
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, sessionId });
|
|
5428
|
+
const { enc, iv, seq: frameSeq } = encryptPayload(contentStr, conn.sessionKey, conn.txSeq.next());
|
|
5429
|
+
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, { type: "markdown", enc, iv, seq: frameSeq, sessionId });
|
|
5050
5430
|
} else {
|
|
5051
5431
|
sendToGroup(conn.activeSocket, conn.activeGroups.privateGroup, payload);
|
|
5052
5432
|
}
|
|
@@ -5105,7 +5485,7 @@ async function runAgent(rawOptions) {
|
|
|
5105
5485
|
dispatcher.setApprovalEnforcer(null);
|
|
5106
5486
|
}
|
|
5107
5487
|
ws.on("open", async () => {
|
|
5108
|
-
|
|
5488
|
+
log15.info("connector connected to relay");
|
|
5109
5489
|
conn.resetReconnectDelay();
|
|
5110
5490
|
ws.send(JSON.stringify({ type: "joinGroup", group: groups.privateGroup }));
|
|
5111
5491
|
ws.send(JSON.stringify({ type: "joinGroup", group: groups.registryGroup }));
|
|
@@ -5116,11 +5496,22 @@ async function runAgent(rawOptions) {
|
|
|
5116
5496
|
});
|
|
5117
5497
|
conn.replayBufferedOutput((chunk) => {
|
|
5118
5498
|
for (const outputChunk of splitTransportChunks(chunk)) {
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5499
|
+
if (conn.sessionKey) {
|
|
5500
|
+
const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
|
|
5501
|
+
sendToGroup(ws, groups.privateGroup, {
|
|
5502
|
+
type: "output",
|
|
5503
|
+
enc,
|
|
5504
|
+
iv,
|
|
5505
|
+
seq,
|
|
5506
|
+
sessionId: ptySessionId
|
|
5507
|
+
});
|
|
5508
|
+
} else {
|
|
5509
|
+
sendToGroup(ws, groups.privateGroup, {
|
|
5510
|
+
type: "output",
|
|
5511
|
+
data: outputChunk,
|
|
5512
|
+
sessionId: ptySessionId
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5124
5515
|
}
|
|
5125
5516
|
});
|
|
5126
5517
|
for (const [sessionId] of sessionStreamer.activeStreams()) {
|
|
@@ -5128,8 +5519,8 @@ async function runAgent(rawOptions) {
|
|
|
5128
5519
|
for (const chunk of buffered) {
|
|
5129
5520
|
for (const outputChunk of splitTransportChunks(chunk)) {
|
|
5130
5521
|
if (conn.sessionKey) {
|
|
5131
|
-
const { enc, iv } = encryptPayload(outputChunk, conn.sessionKey);
|
|
5132
|
-
sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, sessionId });
|
|
5522
|
+
const { enc, iv, seq } = encryptPayload(outputChunk, conn.sessionKey, conn.txSeq.next());
|
|
5523
|
+
sendToGroup(ws, groups.privateGroup, { type: "output", enc, iv, seq, sessionId });
|
|
5133
5524
|
} else {
|
|
5134
5525
|
sendToGroup(ws, groups.privateGroup, { type: "output", data: outputChunk, sessionId });
|
|
5135
5526
|
}
|
|
@@ -5155,9 +5546,26 @@ async function runAgent(rawOptions) {
|
|
|
5155
5546
|
if (payload.type === "input") {
|
|
5156
5547
|
let inputData = null;
|
|
5157
5548
|
if (typeof payload.enc === "string" && typeof payload.iv === "string" && conn.sessionKey) {
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5549
|
+
const inSeq = typeof payload.seq === "number" ? payload.seq : null;
|
|
5550
|
+
const rxSeq = conn.getRxSeqForViewer(payload.viewerId);
|
|
5551
|
+
if (inSeq === null) {
|
|
5552
|
+
log15.warn("rejecting encrypted input frame without seq (#310)");
|
|
5553
|
+
} else if (!rxSeq.accept(inSeq)) {
|
|
5554
|
+
log15.warn("rejecting replayed/out-of-order input", {
|
|
5555
|
+
seq: inSeq,
|
|
5556
|
+
lastSeen: rxSeq.lastSeq,
|
|
5557
|
+
viewerId: typeof payload.viewerId === "string" ? payload.viewerId : "_legacy"
|
|
5558
|
+
});
|
|
5559
|
+
} else {
|
|
5560
|
+
inputData = decryptPayload(
|
|
5561
|
+
payload.enc,
|
|
5562
|
+
payload.iv,
|
|
5563
|
+
conn.sessionKey,
|
|
5564
|
+
inSeq
|
|
5565
|
+
);
|
|
5566
|
+
if (inputData === null) {
|
|
5567
|
+
log15.warn("failed to decrypt input \u2014 ignoring");
|
|
5568
|
+
}
|
|
5161
5569
|
}
|
|
5162
5570
|
} else if (typeof payload.data === "string") {
|
|
5163
5571
|
inputData = payload.data;
|
|
@@ -5187,11 +5595,13 @@ async function runAgent(rawOptions) {
|
|
|
5187
5595
|
}
|
|
5188
5596
|
});
|
|
5189
5597
|
ws.on("error", (error) => {
|
|
5190
|
-
|
|
5598
|
+
log15.error("websocket error", { error: error.message });
|
|
5191
5599
|
});
|
|
5192
5600
|
ws.on("close", (code, reason) => {
|
|
5193
|
-
const
|
|
5194
|
-
|
|
5601
|
+
const fields = {};
|
|
5602
|
+
if (code) fields.code = code;
|
|
5603
|
+
if (reason.length > 0) fields.reason = reason.toString();
|
|
5604
|
+
log15.info("relay disconnected; output will be buffered until reconnect", fields);
|
|
5195
5605
|
conn.cleanup();
|
|
5196
5606
|
resolve();
|
|
5197
5607
|
});
|
|
@@ -5208,35 +5618,35 @@ async function runAgent(rawOptions) {
|
|
|
5208
5618
|
});
|
|
5209
5619
|
inventory.updateOptions(options);
|
|
5210
5620
|
dispatcher.updateOptions(options);
|
|
5211
|
-
|
|
5621
|
+
log15.info("recovered from auth failure via machine credential; reconnecting");
|
|
5212
5622
|
continue;
|
|
5213
5623
|
} catch (mcError) {
|
|
5214
5624
|
if (mcError instanceof AuthError) {
|
|
5215
|
-
|
|
5625
|
+
log15.error("auth recovery failed", { error: mcError.message });
|
|
5216
5626
|
} else {
|
|
5217
5627
|
const mcMsg = mcError instanceof Error ? mcError.message : String(mcError);
|
|
5218
|
-
|
|
5628
|
+
log15.error("machine credential recovery failed", { error: mcMsg });
|
|
5219
5629
|
}
|
|
5220
5630
|
}
|
|
5221
5631
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
"
|
|
5632
|
+
log15.error("auth error", { error: error.message });
|
|
5633
|
+
log15.error(
|
|
5634
|
+
"connector token is invalid or revoked. Stopping. Re-connect with: ragent connect --token <token>"
|
|
5225
5635
|
);
|
|
5226
5636
|
dispatcher.shouldRun = false;
|
|
5227
5637
|
break;
|
|
5228
5638
|
}
|
|
5229
5639
|
const message = error instanceof Error ? error.message : String(error);
|
|
5230
|
-
|
|
5640
|
+
log15.error("relay connect failed", { error: message });
|
|
5231
5641
|
}
|
|
5232
5642
|
if (!dispatcher.shouldRun) break;
|
|
5233
5643
|
if (dispatcher.reconnectRequested) {
|
|
5234
|
-
|
|
5644
|
+
log15.info("reconnecting to relay");
|
|
5235
5645
|
await wait(300);
|
|
5236
5646
|
continue;
|
|
5237
5647
|
}
|
|
5238
5648
|
const jitteredDelay = conn.reconnectDelay * (0.5 + Math.random());
|
|
5239
|
-
|
|
5649
|
+
log15.info("disconnected; reconnecting", { seconds: Math.round(jitteredDelay / 1e3) });
|
|
5240
5650
|
await wait(jitteredDelay);
|
|
5241
5651
|
conn.reconnectDelay = Math.min(conn.reconnectDelay * 1.5, MAX_RECONNECT_DELAY_MS);
|
|
5242
5652
|
}
|
|
@@ -5335,6 +5745,7 @@ function printCommandArt(title, subtitle = "remote agent control") {
|
|
|
5335
5745
|
}
|
|
5336
5746
|
|
|
5337
5747
|
// src/commands/connect.ts
|
|
5748
|
+
var log16 = createLogger("connect");
|
|
5338
5749
|
async function connectMachine(opts) {
|
|
5339
5750
|
const portal = opts.portal || DEFAULT_PORTAL;
|
|
5340
5751
|
const hostId = sanitizeHostId(opts.id || inferHostId());
|
|
@@ -5366,12 +5777,12 @@ async function connectMachine(opts) {
|
|
|
5366
5777
|
nextConfig.refreshExpiresAt = claimed.refreshExpiresAt;
|
|
5367
5778
|
}
|
|
5368
5779
|
saveConfig(nextConfig);
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5780
|
+
log16.info("machine connected", { hostName, hostId });
|
|
5781
|
+
log16.info("machine name (override with --name <friendly-name>)", { hostName });
|
|
5782
|
+
log16.info("config saved", { path: CONFIG_FILE });
|
|
5372
5783
|
if (opts.asService) {
|
|
5373
5784
|
await ensureServiceInstalled({ enable: true, start: true });
|
|
5374
|
-
|
|
5785
|
+
log16.info("service mode enabled");
|
|
5375
5786
|
return;
|
|
5376
5787
|
}
|
|
5377
5788
|
if (opts.run !== false) {
|
|
@@ -5391,13 +5802,14 @@ function registerConnectCommand(program2) {
|
|
|
5391
5802
|
await connectMachine(opts);
|
|
5392
5803
|
} catch (error) {
|
|
5393
5804
|
const message = error instanceof Error ? error.message : String(error);
|
|
5394
|
-
|
|
5805
|
+
log16.error("connect failed", { error: message });
|
|
5395
5806
|
process.exit(1);
|
|
5396
5807
|
}
|
|
5397
5808
|
});
|
|
5398
5809
|
}
|
|
5399
5810
|
|
|
5400
5811
|
// src/commands/run.ts
|
|
5812
|
+
var log17 = createLogger("run");
|
|
5401
5813
|
function registerRunCommand(program2) {
|
|
5402
5814
|
program2.command("run").description("Run connector for a connected machine").option("--portal <url>", "Portal base URL").option("--agent-token <token>", "Connector token").option("-i, --id <id>", "Machine ID").option("-n, --name <name>", "Machine name").option("-c, --command <command>", "CLI command to run").option("--no-redact", "Disable secret redaction in terminal output").action(async (opts) => {
|
|
5403
5815
|
try {
|
|
@@ -5405,7 +5817,7 @@ function registerRunCommand(program2) {
|
|
|
5405
5817
|
await runAgent(opts);
|
|
5406
5818
|
} catch (error) {
|
|
5407
5819
|
const message = error instanceof Error ? error.message : String(error);
|
|
5408
|
-
|
|
5820
|
+
log17.error("run failed", { error: message });
|
|
5409
5821
|
process.exit(1);
|
|
5410
5822
|
}
|
|
5411
5823
|
});
|
|
@@ -5413,6 +5825,7 @@ function registerRunCommand(program2) {
|
|
|
5413
5825
|
|
|
5414
5826
|
// src/commands/doctor.ts
|
|
5415
5827
|
var os10 = __toESM(require("os"));
|
|
5828
|
+
var log18 = createLogger("doctor");
|
|
5416
5829
|
async function runDoctor(opts) {
|
|
5417
5830
|
const options = resolveRunOptions(opts);
|
|
5418
5831
|
const checks = [];
|
|
@@ -5448,29 +5861,27 @@ async function runDoctor(opts) {
|
|
|
5448
5861
|
console.log(`${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`);
|
|
5449
5862
|
});
|
|
5450
5863
|
if (!platformOk) {
|
|
5451
|
-
|
|
5864
|
+
log18.info("Linux is required for the connector");
|
|
5452
5865
|
}
|
|
5453
5866
|
if (!tmuxOk) {
|
|
5454
5867
|
const recipe = await chooseTmuxInstallCommand();
|
|
5455
5868
|
if (recipe) {
|
|
5456
|
-
|
|
5869
|
+
log18.info("tmux install suggestion", { command: recipe.command });
|
|
5457
5870
|
}
|
|
5458
5871
|
if (opts.fix) {
|
|
5459
5872
|
try {
|
|
5460
5873
|
const installed = await installTmuxInteractively();
|
|
5461
5874
|
if (installed) {
|
|
5462
|
-
|
|
5875
|
+
log18.info("tmux installation complete");
|
|
5463
5876
|
} else {
|
|
5464
|
-
|
|
5877
|
+
log18.info("tmux installation skipped or incomplete");
|
|
5465
5878
|
}
|
|
5466
5879
|
} catch (error) {
|
|
5467
5880
|
const message = error instanceof Error ? error.message : String(error);
|
|
5468
|
-
|
|
5881
|
+
log18.error("failed to install tmux", { error: message });
|
|
5469
5882
|
}
|
|
5470
5883
|
} else {
|
|
5471
|
-
|
|
5472
|
-
"[rAgent] Run `ragent doctor --fix` to install missing dependencies interactively."
|
|
5473
|
-
);
|
|
5884
|
+
log18.info("run `ragent doctor --fix` to install missing dependencies interactively");
|
|
5474
5885
|
}
|
|
5475
5886
|
}
|
|
5476
5887
|
}
|
|
@@ -5482,32 +5893,33 @@ function registerDoctorCommand(program2) {
|
|
|
5482
5893
|
}
|
|
5483
5894
|
|
|
5484
5895
|
// src/commands/update.ts
|
|
5896
|
+
var log19 = createLogger("update");
|
|
5485
5897
|
function registerUpdateCommand(program2) {
|
|
5486
5898
|
program2.command("update").description("Update ragent CLI from npm").option("--check", "Check for updates only; do not install").option("--no-restart", "Do not restart the service after updating").action(async (opts) => {
|
|
5487
5899
|
printCommandArt("Update", "checking npm registry for newer ragent-cli versions");
|
|
5488
5900
|
const latestVersion = await checkForUpdate({ force: true });
|
|
5489
5901
|
if (!latestVersion) {
|
|
5490
|
-
|
|
5902
|
+
log19.info("you are up to date", { version: CURRENT_VERSION });
|
|
5491
5903
|
return;
|
|
5492
5904
|
}
|
|
5493
|
-
|
|
5905
|
+
log19.info("update available", { current: CURRENT_VERSION, latest: latestVersion });
|
|
5494
5906
|
if (opts.check) return;
|
|
5495
5907
|
await execAsync(`npm install -g ${shellQuote(PACKAGE_NAME)}`, {
|
|
5496
5908
|
timeout: 5 * 60 * 1e3,
|
|
5497
5909
|
maxBuffer: 10 * 1024 * 1024
|
|
5498
5910
|
});
|
|
5499
|
-
|
|
5911
|
+
log19.info("updated", { version: latestVersion });
|
|
5500
5912
|
const backend = getConfiguredServiceBackend();
|
|
5501
5913
|
if (backend && opts.restart) {
|
|
5502
|
-
|
|
5914
|
+
log19.info("restarting service", { backend });
|
|
5503
5915
|
try {
|
|
5504
5916
|
await restartService();
|
|
5505
5917
|
} catch (err) {
|
|
5506
|
-
|
|
5507
|
-
|
|
5918
|
+
log19.error("failed to restart service", { error: err instanceof Error ? err.message : String(err) });
|
|
5919
|
+
log19.info("please restart manually: ragent service restart");
|
|
5508
5920
|
}
|
|
5509
5921
|
} else if (!backend) {
|
|
5510
|
-
|
|
5922
|
+
log19.info("no service backend detected. If ragent is running manually, restart it to use the new version");
|
|
5511
5923
|
}
|
|
5512
5924
|
});
|
|
5513
5925
|
}
|
|
@@ -5531,6 +5943,7 @@ function registerSessionsCommand(program2) {
|
|
|
5531
5943
|
}
|
|
5532
5944
|
|
|
5533
5945
|
// src/commands/service.ts
|
|
5946
|
+
var log20 = createLogger("service-cmd");
|
|
5534
5947
|
function registerServiceCommand(program2) {
|
|
5535
5948
|
const service = program2.command("service").description("Manage background connector service");
|
|
5536
5949
|
service.command("install").description("Install service (systemd user unit when available)").option("--start", "Start service immediately").option("--no-enable", "Do not enable autostart in systemd").action(async (opts) => {
|
|
@@ -5541,7 +5954,7 @@ function registerServiceCommand(program2) {
|
|
|
5541
5954
|
});
|
|
5542
5955
|
} catch (error) {
|
|
5543
5956
|
const message = error instanceof Error ? error.message : String(error);
|
|
5544
|
-
|
|
5957
|
+
log20.error("service install failed", { error: message });
|
|
5545
5958
|
process.exit(1);
|
|
5546
5959
|
}
|
|
5547
5960
|
});
|
|
@@ -5550,7 +5963,7 @@ function registerServiceCommand(program2) {
|
|
|
5550
5963
|
await startService();
|
|
5551
5964
|
} catch (error) {
|
|
5552
5965
|
const message = error instanceof Error ? error.message : String(error);
|
|
5553
|
-
|
|
5966
|
+
log20.error("service start failed", { error: message });
|
|
5554
5967
|
process.exit(1);
|
|
5555
5968
|
}
|
|
5556
5969
|
});
|
|
@@ -5559,7 +5972,7 @@ function registerServiceCommand(program2) {
|
|
|
5559
5972
|
await stopService();
|
|
5560
5973
|
} catch (error) {
|
|
5561
5974
|
const message = error instanceof Error ? error.message : String(error);
|
|
5562
|
-
|
|
5975
|
+
log20.error("service stop failed", { error: message });
|
|
5563
5976
|
process.exit(1);
|
|
5564
5977
|
}
|
|
5565
5978
|
});
|
|
@@ -5568,7 +5981,7 @@ function registerServiceCommand(program2) {
|
|
|
5568
5981
|
await restartService();
|
|
5569
5982
|
} catch (error) {
|
|
5570
5983
|
const message = error instanceof Error ? error.message : String(error);
|
|
5571
|
-
|
|
5984
|
+
log20.error("service restart failed", { error: message });
|
|
5572
5985
|
process.exit(1);
|
|
5573
5986
|
}
|
|
5574
5987
|
});
|
|
@@ -5577,7 +5990,7 @@ function registerServiceCommand(program2) {
|
|
|
5577
5990
|
await printServiceStatus();
|
|
5578
5991
|
} catch (error) {
|
|
5579
5992
|
const message = error instanceof Error ? error.message : String(error);
|
|
5580
|
-
|
|
5993
|
+
log20.error("service status failed", { error: message });
|
|
5581
5994
|
process.exit(1);
|
|
5582
5995
|
}
|
|
5583
5996
|
});
|
|
@@ -5586,7 +5999,7 @@ function registerServiceCommand(program2) {
|
|
|
5586
5999
|
await printServiceLogs(opts);
|
|
5587
6000
|
} catch (error) {
|
|
5588
6001
|
const message = error instanceof Error ? error.message : String(error);
|
|
5589
|
-
|
|
6002
|
+
log20.error("service logs failed", { error: message });
|
|
5590
6003
|
process.exit(1);
|
|
5591
6004
|
}
|
|
5592
6005
|
});
|
|
@@ -5595,7 +6008,7 @@ function registerServiceCommand(program2) {
|
|
|
5595
6008
|
await uninstallService();
|
|
5596
6009
|
} catch (error) {
|
|
5597
6010
|
const message = error instanceof Error ? error.message : String(error);
|
|
5598
|
-
|
|
6011
|
+
log20.error("service uninstall failed", { error: message });
|
|
5599
6012
|
process.exit(1);
|
|
5600
6013
|
}
|
|
5601
6014
|
});
|
|
@@ -5603,6 +6016,7 @@ function registerServiceCommand(program2) {
|
|
|
5603
6016
|
|
|
5604
6017
|
// src/commands/uninstall.ts
|
|
5605
6018
|
var fs6 = __toESM(require("fs"));
|
|
6019
|
+
var log21 = createLogger("uninstall");
|
|
5606
6020
|
async function uninstallAgent(opts) {
|
|
5607
6021
|
const config = loadConfig();
|
|
5608
6022
|
const hostName = config.hostName || config.hostId || "this machine";
|
|
@@ -5612,12 +6026,12 @@ async function uninstallAgent(opts) {
|
|
|
5612
6026
|
false
|
|
5613
6027
|
);
|
|
5614
6028
|
if (!confirmed) {
|
|
5615
|
-
|
|
6029
|
+
log21.info("uninstall cancelled");
|
|
5616
6030
|
return;
|
|
5617
6031
|
}
|
|
5618
6032
|
}
|
|
5619
6033
|
if (config.portal && config.agentToken) {
|
|
5620
|
-
|
|
6034
|
+
log21.info("deregistering from server");
|
|
5621
6035
|
try {
|
|
5622
6036
|
const token = await refreshTokenIfNeeded({
|
|
5623
6037
|
portal: config.portal,
|
|
@@ -5625,24 +6039,24 @@ async function uninstallAgent(opts) {
|
|
|
5625
6039
|
});
|
|
5626
6040
|
const ok = await deregisterHost({ portal: config.portal, agentToken: token });
|
|
5627
6041
|
if (ok) {
|
|
5628
|
-
|
|
6042
|
+
log21.info("host removed from server");
|
|
5629
6043
|
}
|
|
5630
6044
|
} catch {
|
|
5631
|
-
|
|
6045
|
+
log21.warn("could not reach server \u2014 host may need manual removal from dashboard");
|
|
5632
6046
|
}
|
|
5633
6047
|
}
|
|
5634
|
-
|
|
6048
|
+
log21.info("stopping and removing service");
|
|
5635
6049
|
await uninstallService().catch(() => void 0);
|
|
5636
6050
|
if (fs6.existsSync(CONFIG_DIR)) {
|
|
5637
6051
|
fs6.rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
5638
|
-
|
|
6052
|
+
log21.info("removed config directory", { path: CONFIG_DIR });
|
|
5639
6053
|
}
|
|
5640
6054
|
try {
|
|
5641
6055
|
await execAsync(`npm unlink -g ${PACKAGE_NAME}`, { timeout: 15e3 });
|
|
5642
|
-
|
|
6056
|
+
log21.info("unlinked global package", { package: PACKAGE_NAME });
|
|
5643
6057
|
} catch {
|
|
5644
6058
|
}
|
|
5645
|
-
|
|
6059
|
+
log21.info("uninstall complete. rAgent has been removed from this machine");
|
|
5646
6060
|
}
|
|
5647
6061
|
function registerUninstallCommand(parent) {
|
|
5648
6062
|
parent.command("uninstall").description("Remove rAgent from this machine (stops service, deletes config, unlinks CLI)").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
@@ -5651,7 +6065,7 @@ function registerUninstallCommand(parent) {
|
|
|
5651
6065
|
await uninstallAgent(opts);
|
|
5652
6066
|
} catch (error) {
|
|
5653
6067
|
const message = error instanceof Error ? error.message : String(error);
|
|
5654
|
-
|
|
6068
|
+
log21.error("uninstall failed", { error: message });
|
|
5655
6069
|
process.exit(1);
|
|
5656
6070
|
}
|
|
5657
6071
|
});
|
|
@@ -5710,9 +6124,10 @@ function registerDiscoverCommand(program2) {
|
|
|
5710
6124
|
}
|
|
5711
6125
|
|
|
5712
6126
|
// src/index.ts
|
|
6127
|
+
var log22 = createLogger("cli");
|
|
5713
6128
|
process.on("unhandledRejection", (reason) => {
|
|
5714
6129
|
const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
5715
|
-
|
|
6130
|
+
log22.error("unhandled promise rejection", { error: message });
|
|
5716
6131
|
});
|
|
5717
6132
|
import_commander.program.name("ragent").description("Connect machines to rAgent Live").version(CURRENT_VERSION);
|
|
5718
6133
|
registerConnectCommand(import_commander.program);
|