agentflow-core 0.5.2 → 0.6.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/{chunk-YTMQBL3T.js → chunk-VF4FSBXR.js} +309 -37
- package/dist/cli.cjs +410 -44
- package/dist/cli.js +103 -3
- package/dist/index.cjs +314 -39
- package/dist/index.d.cts +109 -1
- package/dist/index.d.ts +109 -1
- package/dist/index.js +7 -1
- package/package.json +7 -3
|
@@ -209,7 +209,6 @@ function getTraceTree(trace) {
|
|
|
209
209
|
// src/live.ts
|
|
210
210
|
import { existsSync, readdirSync, readFileSync, statSync, watch } from "fs";
|
|
211
211
|
import { basename, join, resolve } from "path";
|
|
212
|
-
import { execSync } from "child_process";
|
|
213
212
|
var C = {
|
|
214
213
|
reset: "\x1B[0m",
|
|
215
214
|
bold: "\x1B[1m",
|
|
@@ -480,18 +479,20 @@ function processJsonFile(file) {
|
|
|
480
479
|
const w = info;
|
|
481
480
|
const status2 = findStatus(w);
|
|
482
481
|
const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
|
|
483
|
-
const
|
|
482
|
+
const rawPid = w.pid;
|
|
483
|
+
const pid = typeof rawPid === "number" ? rawPid : Number(rawPid);
|
|
484
|
+
const validPid = Number.isFinite(pid) && pid > 0;
|
|
484
485
|
let validatedStatus = status2;
|
|
485
486
|
let pidAlive = true;
|
|
486
|
-
if (
|
|
487
|
+
if (validPid && (status2 === "running" || status2 === "ok")) {
|
|
487
488
|
try {
|
|
488
|
-
|
|
489
|
+
process.kill(pid, 0);
|
|
489
490
|
} catch {
|
|
490
491
|
pidAlive = false;
|
|
491
492
|
validatedStatus = "error";
|
|
492
493
|
}
|
|
493
494
|
}
|
|
494
|
-
const pidLabel =
|
|
495
|
+
const pidLabel = validPid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
|
|
495
496
|
const detail2 = pidLabel || extractDetail(w);
|
|
496
497
|
records.push({
|
|
497
498
|
id: name,
|
|
@@ -978,11 +979,14 @@ function render(config) {
|
|
|
978
979
|
writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
979
980
|
flushLines(L);
|
|
980
981
|
}
|
|
981
|
-
function getDistDepth(dt, spanId) {
|
|
982
|
+
function getDistDepth(dt, spanId, visited) {
|
|
982
983
|
if (!spanId) return 0;
|
|
984
|
+
const seen = visited ?? /* @__PURE__ */ new Set();
|
|
985
|
+
if (seen.has(spanId)) return 0;
|
|
986
|
+
seen.add(spanId);
|
|
983
987
|
const g = dt.graphs.get(spanId);
|
|
984
988
|
if (!g || !g.parentSpanId) return 0;
|
|
985
|
-
return 1 + getDistDepth(dt, g.parentSpanId);
|
|
989
|
+
return 1 + getDistDepth(dt, g.parentSpanId, seen);
|
|
986
990
|
}
|
|
987
991
|
function startLive(argv) {
|
|
988
992
|
const config = parseArgs(argv);
|
|
@@ -1015,6 +1019,262 @@ function startLive(argv) {
|
|
|
1015
1019
|
});
|
|
1016
1020
|
}
|
|
1017
1021
|
|
|
1022
|
+
// src/process-audit.ts
|
|
1023
|
+
import { execSync } from "child_process";
|
|
1024
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
1025
|
+
import { basename as basename2, join as join2 } from "path";
|
|
1026
|
+
function isPidAlive(pid) {
|
|
1027
|
+
try {
|
|
1028
|
+
process.kill(pid, 0);
|
|
1029
|
+
return true;
|
|
1030
|
+
} catch {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function pidMatchesName(pid, name) {
|
|
1035
|
+
try {
|
|
1036
|
+
const cmdline = readFileSync2(`/proc/${pid}/cmdline`, "utf8");
|
|
1037
|
+
return cmdline.includes(name);
|
|
1038
|
+
} catch {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function readPidFile(path) {
|
|
1043
|
+
try {
|
|
1044
|
+
const pid = parseInt(readFileSync2(path, "utf8").trim(), 10);
|
|
1045
|
+
return isNaN(pid) ? null : pid;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function auditPidFile(config) {
|
|
1051
|
+
if (!config.pidFile) return null;
|
|
1052
|
+
const pid = readPidFile(config.pidFile);
|
|
1053
|
+
if (pid === null) {
|
|
1054
|
+
return {
|
|
1055
|
+
path: config.pidFile,
|
|
1056
|
+
pid: null,
|
|
1057
|
+
alive: false,
|
|
1058
|
+
matchesProcess: false,
|
|
1059
|
+
stale: !existsSync2(config.pidFile),
|
|
1060
|
+
reason: existsSync2(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const alive = isPidAlive(pid);
|
|
1064
|
+
const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
|
|
1065
|
+
const stale = !alive || alive && !matchesProcess;
|
|
1066
|
+
let reason;
|
|
1067
|
+
if (alive && matchesProcess) {
|
|
1068
|
+
reason = `PID ${pid} alive and matches ${config.processName}`;
|
|
1069
|
+
} else if (alive && !matchesProcess) {
|
|
1070
|
+
reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
|
|
1071
|
+
} else {
|
|
1072
|
+
reason = `PID ${pid} no longer exists`;
|
|
1073
|
+
}
|
|
1074
|
+
return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
|
|
1075
|
+
}
|
|
1076
|
+
function auditSystemd(config) {
|
|
1077
|
+
if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
|
|
1078
|
+
const unit = config.systemdUnit;
|
|
1079
|
+
try {
|
|
1080
|
+
const raw = execSync(
|
|
1081
|
+
`systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
|
|
1082
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
1083
|
+
);
|
|
1084
|
+
const props = {};
|
|
1085
|
+
for (const line of raw.trim().split("\n")) {
|
|
1086
|
+
const [k, ...v] = line.split("=");
|
|
1087
|
+
if (k) props[k.trim()] = v.join("=").trim();
|
|
1088
|
+
}
|
|
1089
|
+
const activeState = props["ActiveState"] ?? "unknown";
|
|
1090
|
+
const subState = props["SubState"] ?? "unknown";
|
|
1091
|
+
const mainPid = parseInt(props["MainPID"] ?? "0", 10);
|
|
1092
|
+
const restarts = parseInt(props["NRestarts"] ?? "0", 10);
|
|
1093
|
+
const result = props["Result"] ?? "unknown";
|
|
1094
|
+
return {
|
|
1095
|
+
unit,
|
|
1096
|
+
activeState,
|
|
1097
|
+
subState,
|
|
1098
|
+
mainPid,
|
|
1099
|
+
restarts,
|
|
1100
|
+
result,
|
|
1101
|
+
crashLooping: activeState === "activating" && subState === "auto-restart",
|
|
1102
|
+
failed: activeState === "failed"
|
|
1103
|
+
};
|
|
1104
|
+
} catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function auditWorkers(config) {
|
|
1109
|
+
if (!config.workersFile || !existsSync2(config.workersFile)) return null;
|
|
1110
|
+
try {
|
|
1111
|
+
const data = JSON.parse(readFileSync2(config.workersFile, "utf8"));
|
|
1112
|
+
const orchPid = data.pid ?? null;
|
|
1113
|
+
const orchAlive = orchPid ? isPidAlive(orchPid) : false;
|
|
1114
|
+
const workers = [];
|
|
1115
|
+
for (const [name, info] of Object.entries(data.tools ?? {})) {
|
|
1116
|
+
const w = info;
|
|
1117
|
+
const wPid = w.pid ?? null;
|
|
1118
|
+
const wAlive = wPid ? isPidAlive(wPid) : false;
|
|
1119
|
+
workers.push({
|
|
1120
|
+
name,
|
|
1121
|
+
pid: wPid,
|
|
1122
|
+
declaredStatus: w.status ?? "unknown",
|
|
1123
|
+
alive: wAlive,
|
|
1124
|
+
stale: w.status === "running" && !wAlive
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
orchestratorPid: orchPid,
|
|
1129
|
+
orchestratorAlive: orchAlive,
|
|
1130
|
+
startedAt: data.started_at ?? "",
|
|
1131
|
+
workers
|
|
1132
|
+
};
|
|
1133
|
+
} catch {
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function getOsProcesses(processName) {
|
|
1138
|
+
try {
|
|
1139
|
+
const raw = execSync(`ps aux`, { encoding: "utf8", timeout: 5e3 });
|
|
1140
|
+
return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
|
|
1141
|
+
const parts = line.trim().split(/\s+/);
|
|
1142
|
+
return {
|
|
1143
|
+
pid: parseInt(parts[1] ?? "0", 10),
|
|
1144
|
+
cpu: parts[2] ?? "0",
|
|
1145
|
+
mem: parts[3] ?? "0",
|
|
1146
|
+
command: parts.slice(10).join(" ")
|
|
1147
|
+
};
|
|
1148
|
+
}).filter((p) => !isNaN(p.pid) && p.pid > 0);
|
|
1149
|
+
} catch {
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function discoverProcessConfig(dirs) {
|
|
1154
|
+
let pidFile;
|
|
1155
|
+
let workersFile;
|
|
1156
|
+
let processName = "";
|
|
1157
|
+
for (const dir of dirs) {
|
|
1158
|
+
if (!existsSync2(dir)) continue;
|
|
1159
|
+
let entries;
|
|
1160
|
+
try {
|
|
1161
|
+
entries = readdirSync2(dir);
|
|
1162
|
+
} catch {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
for (const f of entries) {
|
|
1166
|
+
const fp = join2(dir, f);
|
|
1167
|
+
try {
|
|
1168
|
+
if (!statSync2(fp).isFile()) continue;
|
|
1169
|
+
} catch {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (f.endsWith(".pid") && !pidFile) {
|
|
1173
|
+
pidFile = fp;
|
|
1174
|
+
if (!processName) {
|
|
1175
|
+
processName = basename2(f, ".pid");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
|
|
1179
|
+
workersFile = fp;
|
|
1180
|
+
if (!processName && f !== "workers.json") {
|
|
1181
|
+
processName = basename2(f, "-workers.json");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (!processName && !pidFile && !workersFile) return null;
|
|
1187
|
+
if (!processName) processName = "agent";
|
|
1188
|
+
return { processName, pidFile, workersFile };
|
|
1189
|
+
}
|
|
1190
|
+
function auditProcesses(config) {
|
|
1191
|
+
const pidFile = auditPidFile(config);
|
|
1192
|
+
const systemd = auditSystemd(config);
|
|
1193
|
+
const workers = auditWorkers(config);
|
|
1194
|
+
const osProcesses = getOsProcesses(config.processName);
|
|
1195
|
+
const knownPids = /* @__PURE__ */ new Set();
|
|
1196
|
+
if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
|
|
1197
|
+
if (workers) {
|
|
1198
|
+
if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
|
|
1199
|
+
for (const w of workers.workers) {
|
|
1200
|
+
if (w.pid) knownPids.add(w.pid);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (systemd?.mainPid) knownPids.add(systemd.mainPid);
|
|
1204
|
+
const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
|
|
1205
|
+
const problems = [];
|
|
1206
|
+
if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
|
|
1207
|
+
if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
|
|
1208
|
+
if (systemd?.failed) problems.push("Systemd unit has failed");
|
|
1209
|
+
if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
|
|
1210
|
+
if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
|
|
1211
|
+
problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
|
|
1212
|
+
}
|
|
1213
|
+
if (workers) {
|
|
1214
|
+
for (const w of workers.workers) {
|
|
1215
|
+
if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
|
|
1219
|
+
return { pidFile, systemd, workers, osProcesses, orphans, problems };
|
|
1220
|
+
}
|
|
1221
|
+
function formatAuditReport(result) {
|
|
1222
|
+
const lines = [];
|
|
1223
|
+
lines.push("");
|
|
1224
|
+
lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
1225
|
+
lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
|
|
1226
|
+
lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
1227
|
+
if (result.pidFile) {
|
|
1228
|
+
const pf = result.pidFile;
|
|
1229
|
+
const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
|
|
1230
|
+
lines.push(`
|
|
1231
|
+
PID File: ${pf.path}`);
|
|
1232
|
+
lines.push(` ${icon} ${pf.reason}`);
|
|
1233
|
+
}
|
|
1234
|
+
if (result.systemd) {
|
|
1235
|
+
const sd = result.systemd;
|
|
1236
|
+
const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
|
|
1237
|
+
lines.push(`
|
|
1238
|
+
Systemd: ${sd.unit}`);
|
|
1239
|
+
lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
|
|
1240
|
+
lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
|
|
1241
|
+
}
|
|
1242
|
+
if (result.workers) {
|
|
1243
|
+
const w = result.workers;
|
|
1244
|
+
lines.push(`
|
|
1245
|
+
Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
|
|
1246
|
+
for (const worker of w.workers) {
|
|
1247
|
+
const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
|
|
1248
|
+
lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (result.osProcesses.length > 0) {
|
|
1252
|
+
lines.push(`
|
|
1253
|
+
OS Processes (${result.osProcesses.length} total)`);
|
|
1254
|
+
for (const p of result.osProcesses) {
|
|
1255
|
+
lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (result.orphans.length > 0) {
|
|
1259
|
+
lines.push(`
|
|
1260
|
+
\u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
|
|
1261
|
+
for (const p of result.orphans) {
|
|
1262
|
+
lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
lines.push("");
|
|
1266
|
+
if (result.problems.length === 0) {
|
|
1267
|
+
lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
|
|
1268
|
+
} else {
|
|
1269
|
+
lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
|
|
1270
|
+
for (const p of result.problems) {
|
|
1271
|
+
lines.push(` \u2022 ${p}`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
lines.push("");
|
|
1275
|
+
return lines.join("\n");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1018
1278
|
// src/graph-builder.ts
|
|
1019
1279
|
import { randomUUID } from "crypto";
|
|
1020
1280
|
function deepFreeze(obj) {
|
|
@@ -1243,20 +1503,20 @@ function createGraphBuilder(config) {
|
|
|
1243
1503
|
|
|
1244
1504
|
// src/runner.ts
|
|
1245
1505
|
import { spawnSync } from "child_process";
|
|
1246
|
-
import { existsSync as
|
|
1247
|
-
import { basename as
|
|
1506
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, statSync as statSync3, writeFileSync } from "fs";
|
|
1507
|
+
import { basename as basename3, join as join3, resolve as resolve2 } from "path";
|
|
1248
1508
|
function globToRegex(pattern) {
|
|
1249
1509
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1250
1510
|
return new RegExp(`^${escaped}$`);
|
|
1251
1511
|
}
|
|
1252
1512
|
function snapshotDir(dir, patterns) {
|
|
1253
1513
|
const result = /* @__PURE__ */ new Map();
|
|
1254
|
-
if (!
|
|
1255
|
-
for (const entry of
|
|
1514
|
+
if (!existsSync3(dir)) return result;
|
|
1515
|
+
for (const entry of readdirSync3(dir)) {
|
|
1256
1516
|
if (!patterns.some((re) => re.test(entry))) continue;
|
|
1257
|
-
const full =
|
|
1517
|
+
const full = join3(dir, entry);
|
|
1258
1518
|
try {
|
|
1259
|
-
const stat =
|
|
1519
|
+
const stat = statSync3(full);
|
|
1260
1520
|
if (stat.isFile()) {
|
|
1261
1521
|
result.set(full, stat.mtimeMs);
|
|
1262
1522
|
}
|
|
@@ -1266,7 +1526,7 @@ function snapshotDir(dir, patterns) {
|
|
|
1266
1526
|
return result;
|
|
1267
1527
|
}
|
|
1268
1528
|
function agentIdFromFilename(filePath) {
|
|
1269
|
-
const base =
|
|
1529
|
+
const base = basename3(filePath, ".json");
|
|
1270
1530
|
const cleaned = base.replace(/-state$/, "");
|
|
1271
1531
|
return `alfred-${cleaned}`;
|
|
1272
1532
|
}
|
|
@@ -1371,14 +1631,18 @@ async function runTraced(config) {
|
|
|
1371
1631
|
childBuilder.endNode(childRootId);
|
|
1372
1632
|
allGraphs.push(childBuilder.build());
|
|
1373
1633
|
}
|
|
1374
|
-
if (!
|
|
1634
|
+
if (!existsSync3(resolvedTracesDir)) {
|
|
1375
1635
|
mkdirSync(resolvedTracesDir, { recursive: true });
|
|
1376
1636
|
}
|
|
1377
1637
|
const ts = fileTimestamp();
|
|
1378
1638
|
const tracePaths = [];
|
|
1379
1639
|
for (const graph of allGraphs) {
|
|
1380
1640
|
const filename = `${graph.agentId}-${ts}.json`;
|
|
1381
|
-
const outPath =
|
|
1641
|
+
const outPath = join3(resolvedTracesDir, filename);
|
|
1642
|
+
const resolvedOut = resolve2(outPath);
|
|
1643
|
+
if (!resolvedOut.startsWith(resolvedTracesDir + "/") && resolvedOut !== resolvedTracesDir) {
|
|
1644
|
+
throw new Error(`Path traversal detected: agentId "${graph.agentId}" escapes traces directory`);
|
|
1645
|
+
}
|
|
1382
1646
|
writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
1383
1647
|
tracePaths.push(outPath);
|
|
1384
1648
|
}
|
|
@@ -1397,7 +1661,7 @@ async function runTraced(config) {
|
|
|
1397
1661
|
|
|
1398
1662
|
// src/trace-store.ts
|
|
1399
1663
|
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
1400
|
-
import { join as
|
|
1664
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1401
1665
|
function createTraceStore(dir) {
|
|
1402
1666
|
async function ensureDir() {
|
|
1403
1667
|
await mkdir(dir, { recursive: true });
|
|
@@ -1414,7 +1678,7 @@ function createTraceStore(dir) {
|
|
|
1414
1678
|
for (const file of files) {
|
|
1415
1679
|
if (!file.endsWith(".json")) continue;
|
|
1416
1680
|
try {
|
|
1417
|
-
const content = await readFile(
|
|
1681
|
+
const content = await readFile(join4(dir, file), "utf-8");
|
|
1418
1682
|
const graph = loadGraph(content);
|
|
1419
1683
|
graphs.push(graph);
|
|
1420
1684
|
} catch {
|
|
@@ -1426,13 +1690,18 @@ function createTraceStore(dir) {
|
|
|
1426
1690
|
async save(graph) {
|
|
1427
1691
|
await ensureDir();
|
|
1428
1692
|
const json = graphToJson(graph);
|
|
1429
|
-
const filePath =
|
|
1693
|
+
const filePath = join4(dir, `${graph.id}.json`);
|
|
1694
|
+
const resolvedBase = resolve3(dir);
|
|
1695
|
+
const resolvedPath = resolve3(filePath);
|
|
1696
|
+
if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
|
|
1697
|
+
throw new Error(`Path traversal detected: "${graph.id}" escapes base directory`);
|
|
1698
|
+
}
|
|
1430
1699
|
await writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
|
|
1431
1700
|
return filePath;
|
|
1432
1701
|
},
|
|
1433
1702
|
async get(graphId) {
|
|
1434
1703
|
await ensureDir();
|
|
1435
|
-
const filePath =
|
|
1704
|
+
const filePath = join4(dir, `${graphId}.json`);
|
|
1436
1705
|
try {
|
|
1437
1706
|
const content = await readFile(filePath, "utf-8");
|
|
1438
1707
|
return loadGraph(content);
|
|
@@ -1625,9 +1894,9 @@ function toTimeline(graph) {
|
|
|
1625
1894
|
}
|
|
1626
1895
|
|
|
1627
1896
|
// src/watch.ts
|
|
1628
|
-
import { existsSync as
|
|
1897
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1629
1898
|
import { hostname } from "os";
|
|
1630
|
-
import { join as
|
|
1899
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
1631
1900
|
|
|
1632
1901
|
// src/watch-alerts.ts
|
|
1633
1902
|
import { exec } from "child_process";
|
|
@@ -1685,7 +1954,7 @@ function sendTelegram(payload, botToken, chatId) {
|
|
|
1685
1954
|
text: formatTelegram(payload),
|
|
1686
1955
|
parse_mode: "Markdown"
|
|
1687
1956
|
});
|
|
1688
|
-
return new Promise((
|
|
1957
|
+
return new Promise((resolve5, reject) => {
|
|
1689
1958
|
const req = httpsRequest(
|
|
1690
1959
|
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1691
1960
|
{
|
|
@@ -1694,7 +1963,7 @@ function sendTelegram(payload, botToken, chatId) {
|
|
|
1694
1963
|
},
|
|
1695
1964
|
(res) => {
|
|
1696
1965
|
res.resume();
|
|
1697
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
1966
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1698
1967
|
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1699
1968
|
}
|
|
1700
1969
|
);
|
|
@@ -1707,7 +1976,7 @@ function sendWebhook(payload, url) {
|
|
|
1707
1976
|
const body = JSON.stringify(payload);
|
|
1708
1977
|
const isHttps = url.startsWith("https");
|
|
1709
1978
|
const doRequest = isHttps ? httpsRequest : httpRequest;
|
|
1710
|
-
return new Promise((
|
|
1979
|
+
return new Promise((resolve5, reject) => {
|
|
1711
1980
|
const req = doRequest(
|
|
1712
1981
|
url,
|
|
1713
1982
|
{
|
|
@@ -1716,7 +1985,7 @@ function sendWebhook(payload, url) {
|
|
|
1716
1985
|
},
|
|
1717
1986
|
(res) => {
|
|
1718
1987
|
res.resume();
|
|
1719
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300)
|
|
1988
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1720
1989
|
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1721
1990
|
}
|
|
1722
1991
|
);
|
|
@@ -1729,7 +1998,7 @@ function sendWebhook(payload, url) {
|
|
|
1729
1998
|
});
|
|
1730
1999
|
}
|
|
1731
2000
|
function sendCommand(payload, cmd) {
|
|
1732
|
-
return new Promise((
|
|
2001
|
+
return new Promise((resolve5, reject) => {
|
|
1733
2002
|
const env = {
|
|
1734
2003
|
...process.env,
|
|
1735
2004
|
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
@@ -1742,13 +2011,13 @@ function sendCommand(payload, cmd) {
|
|
|
1742
2011
|
};
|
|
1743
2012
|
exec(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1744
2013
|
if (err) reject(err);
|
|
1745
|
-
else
|
|
2014
|
+
else resolve5();
|
|
1746
2015
|
});
|
|
1747
2016
|
});
|
|
1748
2017
|
}
|
|
1749
2018
|
|
|
1750
2019
|
// src/watch-state.ts
|
|
1751
|
-
import { existsSync as
|
|
2020
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1752
2021
|
function parseDuration(input) {
|
|
1753
2022
|
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
|
|
1754
2023
|
if (!match) {
|
|
@@ -1773,9 +2042,9 @@ function emptyState() {
|
|
|
1773
2042
|
return { version: 1, agents: {}, lastPollTime: 0 };
|
|
1774
2043
|
}
|
|
1775
2044
|
function loadWatchState(filePath) {
|
|
1776
|
-
if (!
|
|
2045
|
+
if (!existsSync4(filePath)) return emptyState();
|
|
1777
2046
|
try {
|
|
1778
|
-
const raw = JSON.parse(
|
|
2047
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf8"));
|
|
1779
2048
|
if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
|
|
1780
2049
|
return raw;
|
|
1781
2050
|
} catch {
|
|
@@ -2017,20 +2286,20 @@ function parseWatchArgs(argv) {
|
|
|
2017
2286
|
recursive = true;
|
|
2018
2287
|
i++;
|
|
2019
2288
|
} else if (!arg.startsWith("-")) {
|
|
2020
|
-
dirs.push(
|
|
2289
|
+
dirs.push(resolve4(arg));
|
|
2021
2290
|
i++;
|
|
2022
2291
|
} else {
|
|
2023
2292
|
i++;
|
|
2024
2293
|
}
|
|
2025
2294
|
}
|
|
2026
|
-
if (dirs.length === 0) dirs.push(
|
|
2295
|
+
if (dirs.length === 0) dirs.push(resolve4("."));
|
|
2027
2296
|
if (alertConditions.length === 0) {
|
|
2028
2297
|
alertConditions.push({ type: "error" });
|
|
2029
2298
|
alertConditions.push({ type: "recovery" });
|
|
2030
2299
|
}
|
|
2031
2300
|
notifyChannels.unshift({ type: "stdout" });
|
|
2032
2301
|
if (!stateFilePath) {
|
|
2033
|
-
stateFilePath =
|
|
2302
|
+
stateFilePath = join5(dirs[0], ".agentflow-watch-state.json");
|
|
2034
2303
|
}
|
|
2035
2304
|
return {
|
|
2036
2305
|
dirs,
|
|
@@ -2038,7 +2307,7 @@ function parseWatchArgs(argv) {
|
|
|
2038
2307
|
pollIntervalMs,
|
|
2039
2308
|
alertConditions,
|
|
2040
2309
|
notifyChannels,
|
|
2041
|
-
stateFilePath:
|
|
2310
|
+
stateFilePath: resolve4(stateFilePath),
|
|
2042
2311
|
cooldownMs
|
|
2043
2312
|
};
|
|
2044
2313
|
}
|
|
@@ -2092,12 +2361,12 @@ Examples:
|
|
|
2092
2361
|
}
|
|
2093
2362
|
function startWatch(argv) {
|
|
2094
2363
|
const config = parseWatchArgs(argv);
|
|
2095
|
-
const valid = config.dirs.filter((d) =>
|
|
2364
|
+
const valid = config.dirs.filter((d) => existsSync5(d));
|
|
2096
2365
|
if (valid.length === 0) {
|
|
2097
2366
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
2098
2367
|
process.exit(1);
|
|
2099
2368
|
}
|
|
2100
|
-
const invalid = config.dirs.filter((d) => !
|
|
2369
|
+
const invalid = config.dirs.filter((d) => !existsSync5(d));
|
|
2101
2370
|
if (invalid.length > 0) {
|
|
2102
2371
|
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
2103
2372
|
}
|
|
@@ -2191,6 +2460,9 @@ export {
|
|
|
2191
2460
|
stitchTrace,
|
|
2192
2461
|
getTraceTree,
|
|
2193
2462
|
startLive,
|
|
2463
|
+
discoverProcessConfig,
|
|
2464
|
+
auditProcesses,
|
|
2465
|
+
formatAuditReport,
|
|
2194
2466
|
createGraphBuilder,
|
|
2195
2467
|
runTraced,
|
|
2196
2468
|
createTraceStore,
|