openclaw-aegis 1.0.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2063 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1064 -0
- package/dist/index.js +2203 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,2063 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
var import_commander5 = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/cli/commands/status.ts
|
|
30
|
+
var import_commander = require("commander");
|
|
31
|
+
|
|
32
|
+
// src/config/loader.ts
|
|
33
|
+
var fs = __toESM(require("fs"));
|
|
34
|
+
var path = __toESM(require("path"));
|
|
35
|
+
var os = __toESM(require("os"));
|
|
36
|
+
var import_toml = __toESM(require("@iarna/toml"));
|
|
37
|
+
|
|
38
|
+
// src/config/schema.ts
|
|
39
|
+
var import_zod = require("zod");
|
|
40
|
+
var alertChannelSchema = import_zod.z.discriminatedUnion("type", [
|
|
41
|
+
import_zod.z.object({
|
|
42
|
+
type: import_zod.z.literal("ntfy"),
|
|
43
|
+
url: import_zod.z.string().url().default("https://ntfy.sh"),
|
|
44
|
+
topic: import_zod.z.string().min(1),
|
|
45
|
+
priority: import_zod.z.number().int().min(1).max(5).default(4)
|
|
46
|
+
}),
|
|
47
|
+
import_zod.z.object({
|
|
48
|
+
type: import_zod.z.literal("webhook"),
|
|
49
|
+
url: import_zod.z.string().url(),
|
|
50
|
+
secret: import_zod.z.string().min(1).optional()
|
|
51
|
+
}),
|
|
52
|
+
import_zod.z.object({
|
|
53
|
+
type: import_zod.z.literal("telegram"),
|
|
54
|
+
botToken: import_zod.z.string().min(1),
|
|
55
|
+
chatId: import_zod.z.string().min(1)
|
|
56
|
+
}),
|
|
57
|
+
import_zod.z.object({
|
|
58
|
+
type: import_zod.z.literal("whatsapp"),
|
|
59
|
+
phoneNumberId: import_zod.z.string().min(1),
|
|
60
|
+
accessToken: import_zod.z.string().min(1),
|
|
61
|
+
recipientNumber: import_zod.z.string().min(1)
|
|
62
|
+
}),
|
|
63
|
+
import_zod.z.object({
|
|
64
|
+
type: import_zod.z.literal("slack"),
|
|
65
|
+
webhookUrl: import_zod.z.string().url(),
|
|
66
|
+
channel: import_zod.z.string().optional()
|
|
67
|
+
}),
|
|
68
|
+
import_zod.z.object({
|
|
69
|
+
type: import_zod.z.literal("discord"),
|
|
70
|
+
webhookUrl: import_zod.z.string().url(),
|
|
71
|
+
username: import_zod.z.string().optional()
|
|
72
|
+
}),
|
|
73
|
+
import_zod.z.object({
|
|
74
|
+
type: import_zod.z.literal("email"),
|
|
75
|
+
host: import_zod.z.string().min(1),
|
|
76
|
+
port: import_zod.z.number().int().min(1).max(65535).default(587),
|
|
77
|
+
secure: import_zod.z.boolean().default(false),
|
|
78
|
+
username: import_zod.z.string().min(1),
|
|
79
|
+
password: import_zod.z.string().min(1),
|
|
80
|
+
from: import_zod.z.string().min(1),
|
|
81
|
+
to: import_zod.z.string().min(1)
|
|
82
|
+
}),
|
|
83
|
+
import_zod.z.object({
|
|
84
|
+
type: import_zod.z.literal("pushover"),
|
|
85
|
+
apiToken: import_zod.z.string().min(1),
|
|
86
|
+
userKey: import_zod.z.string().min(1),
|
|
87
|
+
device: import_zod.z.string().optional()
|
|
88
|
+
})
|
|
89
|
+
]);
|
|
90
|
+
var aegisConfigSchema = import_zod.z.object({
|
|
91
|
+
gateway: import_zod.z.object({
|
|
92
|
+
configPath: import_zod.z.string().default("~/.openclaw/openclaw.json"),
|
|
93
|
+
pidFile: import_zod.z.string().default("openclaw-gateway.service"),
|
|
94
|
+
port: import_zod.z.number().int().min(1).max(65535).default(18789),
|
|
95
|
+
logPath: import_zod.z.string().default("~/.openclaw/logs/gateway.log"),
|
|
96
|
+
healthEndpoint: import_zod.z.string().default("/health")
|
|
97
|
+
}).default({}),
|
|
98
|
+
monitoring: import_zod.z.object({
|
|
99
|
+
intervalMs: import_zod.z.number().int().min(1e3).default(1e4),
|
|
100
|
+
probeTimeoutMs: import_zod.z.number().int().min(500).default(5e3),
|
|
101
|
+
configPollIntervalMs: import_zod.z.number().int().min(500).default(2e3),
|
|
102
|
+
degradedConfirmationCount: import_zod.z.number().int().min(1).default(2)
|
|
103
|
+
}).default({}),
|
|
104
|
+
health: import_zod.z.object({
|
|
105
|
+
healthyMin: import_zod.z.number().int().min(0).default(7),
|
|
106
|
+
degradedMin: import_zod.z.number().int().min(0).default(4),
|
|
107
|
+
memoryThresholdMb: import_zod.z.number().int().min(0).default(512),
|
|
108
|
+
cpuThresholdPercent: import_zod.z.number().int().min(0).max(100).default(90),
|
|
109
|
+
diskThresholdMb: import_zod.z.number().int().min(0).default(100)
|
|
110
|
+
}).default({}),
|
|
111
|
+
recovery: import_zod.z.object({
|
|
112
|
+
l1MaxAttempts: import_zod.z.number().int().min(1).default(3),
|
|
113
|
+
l1BackoffBaseMs: import_zod.z.number().int().min(1e3).default(5e3),
|
|
114
|
+
l1BackoffMultiplier: import_zod.z.number().min(1).default(3),
|
|
115
|
+
l2MaxAttempts: import_zod.z.number().int().min(1).default(2),
|
|
116
|
+
l2CooldownMs: import_zod.z.number().int().min(1e3).default(6e4),
|
|
117
|
+
circuitBreakerMaxCycles: import_zod.z.number().int().min(1).default(3),
|
|
118
|
+
circuitBreakerWindowMs: import_zod.z.number().int().min(6e4).default(36e5),
|
|
119
|
+
antiFlap: import_zod.z.object({
|
|
120
|
+
maxRestarts: import_zod.z.number().int().min(1).default(5),
|
|
121
|
+
windowMs: import_zod.z.number().int().min(6e4).default(9e5),
|
|
122
|
+
cooldownMs: import_zod.z.number().int().min(6e4).default(6e5),
|
|
123
|
+
decayMs: import_zod.z.number().int().min(6e4).default(216e5)
|
|
124
|
+
}).default({})
|
|
125
|
+
}).default({}),
|
|
126
|
+
backup: import_zod.z.object({
|
|
127
|
+
maxChronological: import_zod.z.number().int().min(1).default(20),
|
|
128
|
+
maxKnownGood: import_zod.z.number().int().min(1).default(3),
|
|
129
|
+
knownGoodStabilityMs: import_zod.z.number().int().min(1e4).default(6e4),
|
|
130
|
+
basePath: import_zod.z.string().default("~/.openclaw/aegis/backups")
|
|
131
|
+
}).default({}),
|
|
132
|
+
deadManSwitch: import_zod.z.object({
|
|
133
|
+
countdownMs: import_zod.z.number().int().min(5e3).default(3e4),
|
|
134
|
+
enabled: import_zod.z.boolean().default(true)
|
|
135
|
+
}).default({}),
|
|
136
|
+
alerts: import_zod.z.object({
|
|
137
|
+
channels: import_zod.z.array(alertChannelSchema).default([]),
|
|
138
|
+
retryAttempts: import_zod.z.number().int().min(0).default(3),
|
|
139
|
+
retryBackoffMs: import_zod.z.array(import_zod.z.number().int()).default([5e3, 15e3, 45e3])
|
|
140
|
+
}).default({}),
|
|
141
|
+
platform: import_zod.z.object({
|
|
142
|
+
type: import_zod.z.enum(["systemd", "launchd"]).default("systemd"),
|
|
143
|
+
watchdogSec: import_zod.z.number().int().min(10).default(30)
|
|
144
|
+
}).default({})
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// src/config/loader.ts
|
|
148
|
+
function expandHome(filepath) {
|
|
149
|
+
if (filepath.startsWith("~/")) {
|
|
150
|
+
return path.join(os.homedir(), filepath.slice(2));
|
|
151
|
+
}
|
|
152
|
+
return filepath;
|
|
153
|
+
}
|
|
154
|
+
function resolveConfigPaths(config) {
|
|
155
|
+
return {
|
|
156
|
+
...config,
|
|
157
|
+
gateway: {
|
|
158
|
+
...config.gateway,
|
|
159
|
+
configPath: expandHome(config.gateway.configPath),
|
|
160
|
+
pidFile: expandHome(config.gateway.pidFile),
|
|
161
|
+
logPath: expandHome(config.gateway.logPath)
|
|
162
|
+
},
|
|
163
|
+
backup: {
|
|
164
|
+
...config.backup,
|
|
165
|
+
basePath: expandHome(config.backup.basePath)
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function loadConfig(configPath) {
|
|
170
|
+
const resolvedPath = expandHome(configPath);
|
|
171
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
172
|
+
const defaults = aegisConfigSchema.parse({});
|
|
173
|
+
return resolveConfigPaths(defaults);
|
|
174
|
+
}
|
|
175
|
+
const raw = fs.readFileSync(resolvedPath, "utf-8");
|
|
176
|
+
const parsed = import_toml.default.parse(raw);
|
|
177
|
+
const validated = aegisConfigSchema.parse(parsed);
|
|
178
|
+
return resolveConfigPaths(validated);
|
|
179
|
+
}
|
|
180
|
+
var DEFAULT_CONFIG_PATH = "~/.openclaw/aegis/config.toml";
|
|
181
|
+
function getConfigDir() {
|
|
182
|
+
return expandHome("~/.openclaw/aegis");
|
|
183
|
+
}
|
|
184
|
+
function ensureConfigDir() {
|
|
185
|
+
const dir = getConfigDir();
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/health/monitor.ts
|
|
190
|
+
var import_node_events = require("events");
|
|
191
|
+
|
|
192
|
+
// src/types/index.ts
|
|
193
|
+
var PROBE_WEIGHTS = {
|
|
194
|
+
process: 2,
|
|
195
|
+
port: 2,
|
|
196
|
+
http: 2,
|
|
197
|
+
config: 2,
|
|
198
|
+
websocket: 1,
|
|
199
|
+
tun: 1,
|
|
200
|
+
memory: 1,
|
|
201
|
+
cpu: 1,
|
|
202
|
+
disk: 1,
|
|
203
|
+
logTail: 1
|
|
204
|
+
};
|
|
205
|
+
var MAX_HEALTH_SCORE = Object.values(PROBE_WEIGHTS).reduce((a, b) => a + b, 0) * 2;
|
|
206
|
+
|
|
207
|
+
// src/health/scoring.ts
|
|
208
|
+
var DEFAULT_THRESHOLDS = {
|
|
209
|
+
healthyMin: 7,
|
|
210
|
+
degradedMin: 4
|
|
211
|
+
};
|
|
212
|
+
function computeHealthScore(results, thresholds = DEFAULT_THRESHOLDS) {
|
|
213
|
+
let rawTotal = 0;
|
|
214
|
+
for (const result of results) {
|
|
215
|
+
const weight = PROBE_WEIGHTS[result.name] ?? 1;
|
|
216
|
+
rawTotal += result.score * weight;
|
|
217
|
+
}
|
|
218
|
+
const normalized = MAX_HEALTH_SCORE > 0 ? Math.round(rawTotal / MAX_HEALTH_SCORE * 10) : 0;
|
|
219
|
+
const band = classifyBand(normalized, thresholds);
|
|
220
|
+
return { total: normalized, band, probeResults: results };
|
|
221
|
+
}
|
|
222
|
+
function classifyBand(score, thresholds) {
|
|
223
|
+
if (score >= thresholds.healthyMin) return "healthy";
|
|
224
|
+
if (score >= thresholds.degradedMin) return "degraded";
|
|
225
|
+
return "critical";
|
|
226
|
+
}
|
|
227
|
+
var DegradedConfirmation = class {
|
|
228
|
+
consecutiveDegradedCount = 0;
|
|
229
|
+
requiredCount;
|
|
230
|
+
constructor(requiredCount = 2) {
|
|
231
|
+
this.requiredCount = requiredCount;
|
|
232
|
+
}
|
|
233
|
+
update(band) {
|
|
234
|
+
if (band === "degraded") {
|
|
235
|
+
this.consecutiveDegradedCount++;
|
|
236
|
+
return this.consecutiveDegradedCount >= this.requiredCount;
|
|
237
|
+
}
|
|
238
|
+
if (band === "critical") {
|
|
239
|
+
this.consecutiveDegradedCount = 0;
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
this.consecutiveDegradedCount = 0;
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
reset() {
|
|
246
|
+
this.consecutiveDegradedCount = 0;
|
|
247
|
+
}
|
|
248
|
+
getCount() {
|
|
249
|
+
return this.consecutiveDegradedCount;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/health/probes/resolve-pid.ts
|
|
254
|
+
var fs2 = __toESM(require("fs"));
|
|
255
|
+
var import_node_child_process = require("child_process");
|
|
256
|
+
function resolvePid(pidSource) {
|
|
257
|
+
if (pidSource.endsWith(".service") || !pidSource.includes("/")) {
|
|
258
|
+
const pid = resolveFromSystemd(pidSource);
|
|
259
|
+
if (pid !== null) return pid;
|
|
260
|
+
}
|
|
261
|
+
return resolveFromFile(pidSource);
|
|
262
|
+
}
|
|
263
|
+
function resolveFromSystemd(unit) {
|
|
264
|
+
try {
|
|
265
|
+
const output = (0, import_node_child_process.execFileSync)(
|
|
266
|
+
"systemctl",
|
|
267
|
+
["--user", "show", "-p", "MainPID", "--value", unit],
|
|
268
|
+
{ encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }
|
|
269
|
+
).trim();
|
|
270
|
+
const pid = parseInt(output, 10);
|
|
271
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
272
|
+
return null;
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function resolveFromFile(pidFile) {
|
|
278
|
+
try {
|
|
279
|
+
if (!fs2.existsSync(pidFile)) return null;
|
|
280
|
+
const pidStr = fs2.readFileSync(pidFile, "utf-8").trim();
|
|
281
|
+
const pid = parseInt(pidStr, 10);
|
|
282
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
283
|
+
return null;
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/health/probes/process.ts
|
|
290
|
+
async function processProbe(_target, pidSource) {
|
|
291
|
+
const start = Date.now();
|
|
292
|
+
try {
|
|
293
|
+
const pid = resolvePid(pidSource);
|
|
294
|
+
if (pid === null) {
|
|
295
|
+
return {
|
|
296
|
+
name: "process",
|
|
297
|
+
healthy: false,
|
|
298
|
+
score: 0,
|
|
299
|
+
message: "Gateway process not found (checked systemd unit and PID file)",
|
|
300
|
+
latencyMs: Date.now() - start
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
process.kill(pid, 0);
|
|
305
|
+
return {
|
|
306
|
+
name: "process",
|
|
307
|
+
healthy: true,
|
|
308
|
+
score: 2,
|
|
309
|
+
latencyMs: Date.now() - start
|
|
310
|
+
};
|
|
311
|
+
} catch {
|
|
312
|
+
return {
|
|
313
|
+
name: "process",
|
|
314
|
+
healthy: false,
|
|
315
|
+
score: 0,
|
|
316
|
+
message: `Process ${pid} not running (stale PID)`,
|
|
317
|
+
latencyMs: Date.now() - start
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return {
|
|
322
|
+
name: "process",
|
|
323
|
+
healthy: false,
|
|
324
|
+
score: 0,
|
|
325
|
+
message: `Process probe error: ${err instanceof Error ? err.message : String(err)}`,
|
|
326
|
+
latencyMs: Date.now() - start
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/health/probes/port.ts
|
|
332
|
+
var net = __toESM(require("net"));
|
|
333
|
+
async function portProbe(target, port, timeoutMs = 5e3) {
|
|
334
|
+
const start = Date.now();
|
|
335
|
+
const host = target.type === "remote" ? target.host : "127.0.0.1";
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
const socket = new net.Socket();
|
|
338
|
+
let resolved = false;
|
|
339
|
+
const finish = (result) => {
|
|
340
|
+
if (!resolved) {
|
|
341
|
+
resolved = true;
|
|
342
|
+
socket.destroy();
|
|
343
|
+
resolve(result);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
socket.setTimeout(timeoutMs);
|
|
347
|
+
socket.connect(port, host, () => {
|
|
348
|
+
finish({
|
|
349
|
+
name: "port",
|
|
350
|
+
healthy: true,
|
|
351
|
+
score: 2,
|
|
352
|
+
latencyMs: Date.now() - start
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
socket.on("error", (err) => {
|
|
356
|
+
finish({
|
|
357
|
+
name: "port",
|
|
358
|
+
healthy: false,
|
|
359
|
+
score: 0,
|
|
360
|
+
message: `Port ${port} unreachable: ${err.message}`,
|
|
361
|
+
latencyMs: Date.now() - start
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
socket.on("timeout", () => {
|
|
365
|
+
finish({
|
|
366
|
+
name: "port",
|
|
367
|
+
healthy: false,
|
|
368
|
+
score: 0,
|
|
369
|
+
message: `Port ${port} connection timed out after ${timeoutMs}ms`,
|
|
370
|
+
latencyMs: Date.now() - start
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/health/probes/http.ts
|
|
377
|
+
var http = __toESM(require("http"));
|
|
378
|
+
async function httpHealthProbe(target, port, endpoint = "/health", timeoutMs = 5e3) {
|
|
379
|
+
const start = Date.now();
|
|
380
|
+
const host = target.type === "remote" ? target.host : "127.0.0.1";
|
|
381
|
+
return new Promise((resolve) => {
|
|
382
|
+
const req = http.get(
|
|
383
|
+
{
|
|
384
|
+
hostname: host,
|
|
385
|
+
port,
|
|
386
|
+
path: endpoint,
|
|
387
|
+
timeout: timeoutMs
|
|
388
|
+
},
|
|
389
|
+
(res) => {
|
|
390
|
+
const statusCode = res.statusCode ?? 0;
|
|
391
|
+
res.resume();
|
|
392
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
393
|
+
resolve({
|
|
394
|
+
name: "http",
|
|
395
|
+
healthy: true,
|
|
396
|
+
score: 2,
|
|
397
|
+
latencyMs: Date.now() - start
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
resolve({
|
|
401
|
+
name: "http",
|
|
402
|
+
healthy: false,
|
|
403
|
+
score: 0,
|
|
404
|
+
message: `HTTP health returned status ${statusCode}`,
|
|
405
|
+
latencyMs: Date.now() - start
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
req.on("error", (err) => {
|
|
411
|
+
resolve({
|
|
412
|
+
name: "http",
|
|
413
|
+
healthy: false,
|
|
414
|
+
score: 0,
|
|
415
|
+
message: `HTTP health probe failed: ${err.message}`,
|
|
416
|
+
latencyMs: Date.now() - start
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
req.on("timeout", () => {
|
|
420
|
+
req.destroy();
|
|
421
|
+
resolve({
|
|
422
|
+
name: "http",
|
|
423
|
+
healthy: false,
|
|
424
|
+
score: 0,
|
|
425
|
+
message: `HTTP health probe timed out after ${timeoutMs}ms`,
|
|
426
|
+
latencyMs: Date.now() - start
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/health/probes/config.ts
|
|
433
|
+
var fs3 = __toESM(require("fs"));
|
|
434
|
+
var REQUIRED_CONFIG_PATHS = [
|
|
435
|
+
{ path: ["gateway", "port"], label: "gateway.port" }
|
|
436
|
+
];
|
|
437
|
+
var POISON_KEYS = ["autoAck", "autoAckMessage"];
|
|
438
|
+
async function configProbe(_target, configPath) {
|
|
439
|
+
const start = Date.now();
|
|
440
|
+
try {
|
|
441
|
+
if (!fs3.existsSync(configPath)) {
|
|
442
|
+
return {
|
|
443
|
+
name: "config",
|
|
444
|
+
healthy: false,
|
|
445
|
+
score: 0,
|
|
446
|
+
message: "Gateway config file not found",
|
|
447
|
+
latencyMs: Date.now() - start
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const raw = fs3.readFileSync(configPath, "utf-8");
|
|
451
|
+
let parsed;
|
|
452
|
+
try {
|
|
453
|
+
parsed = JSON.parse(raw);
|
|
454
|
+
} catch {
|
|
455
|
+
return {
|
|
456
|
+
name: "config",
|
|
457
|
+
healthy: false,
|
|
458
|
+
score: 0,
|
|
459
|
+
message: "Gateway config is not valid JSON",
|
|
460
|
+
latencyMs: Date.now() - start
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const missingPaths = REQUIRED_CONFIG_PATHS.filter(({ path: path3 }) => {
|
|
464
|
+
let obj = parsed;
|
|
465
|
+
for (const key of path3) {
|
|
466
|
+
if (obj === null || typeof obj !== "object" || !(key in obj)) return true;
|
|
467
|
+
obj = obj[key];
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
});
|
|
471
|
+
if (missingPaths.length > 0) {
|
|
472
|
+
return {
|
|
473
|
+
name: "config",
|
|
474
|
+
healthy: false,
|
|
475
|
+
score: 0,
|
|
476
|
+
message: `Missing required config keys: ${missingPaths.map((p) => p.label).join(", ")}`,
|
|
477
|
+
latencyMs: Date.now() - start
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const foundPoison = POISON_KEYS.filter((key) => key in parsed);
|
|
481
|
+
if (foundPoison.length > 0) {
|
|
482
|
+
return {
|
|
483
|
+
name: "config",
|
|
484
|
+
healthy: false,
|
|
485
|
+
score: 0,
|
|
486
|
+
message: `Poison keys detected: ${foundPoison.join(", ")}`,
|
|
487
|
+
latencyMs: Date.now() - start
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
name: "config",
|
|
492
|
+
healthy: true,
|
|
493
|
+
score: 2,
|
|
494
|
+
latencyMs: Date.now() - start
|
|
495
|
+
};
|
|
496
|
+
} catch (err) {
|
|
497
|
+
return {
|
|
498
|
+
name: "config",
|
|
499
|
+
healthy: false,
|
|
500
|
+
score: 0,
|
|
501
|
+
message: `Config probe error: ${err instanceof Error ? err.message : String(err)}`,
|
|
502
|
+
latencyMs: Date.now() - start
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/health/probes/tun.ts
|
|
508
|
+
var fs4 = __toESM(require("fs"));
|
|
509
|
+
var import_node_child_process2 = require("child_process");
|
|
510
|
+
var import_node_util = require("util");
|
|
511
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
512
|
+
async function tunProbe(_target) {
|
|
513
|
+
const start = Date.now();
|
|
514
|
+
try {
|
|
515
|
+
const tunDevices = fs4.readdirSync("/sys/class/net").filter((dev) => {
|
|
516
|
+
try {
|
|
517
|
+
const type = fs4.readFileSync(`/sys/class/net/${dev}/type`, "utf-8").trim();
|
|
518
|
+
return type === "65534";
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
if (tunDevices.length === 0) {
|
|
524
|
+
return {
|
|
525
|
+
name: "tun",
|
|
526
|
+
healthy: true,
|
|
527
|
+
score: 2,
|
|
528
|
+
message: "No TUN device configured (not required)",
|
|
529
|
+
latencyMs: Date.now() - start
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const tunDev = tunDevices[0];
|
|
533
|
+
if (!tunDev) {
|
|
534
|
+
return {
|
|
535
|
+
name: "tun",
|
|
536
|
+
healthy: true,
|
|
537
|
+
score: 2,
|
|
538
|
+
message: "No TUN device configured (not required)",
|
|
539
|
+
latencyMs: Date.now() - start
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const { stdout } = await execFileAsync("ip", ["link", "show", tunDev]);
|
|
543
|
+
const isUp = stdout.includes("state UP") || stdout.includes(",UP");
|
|
544
|
+
return {
|
|
545
|
+
name: "tun",
|
|
546
|
+
healthy: isUp,
|
|
547
|
+
score: isUp ? 2 : 0,
|
|
548
|
+
message: isUp ? void 0 : `TUN device ${tunDev} is DOWN`,
|
|
549
|
+
latencyMs: Date.now() - start
|
|
550
|
+
};
|
|
551
|
+
} catch {
|
|
552
|
+
return {
|
|
553
|
+
name: "tun",
|
|
554
|
+
healthy: true,
|
|
555
|
+
score: 2,
|
|
556
|
+
message: "TUN probe skipped (not available on this platform)",
|
|
557
|
+
latencyMs: Date.now() - start
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/health/probes/memory.ts
|
|
563
|
+
var fs5 = __toESM(require("fs"));
|
|
564
|
+
async function memoryProbe(_target, pidSource, thresholdMb = 512) {
|
|
565
|
+
const start = Date.now();
|
|
566
|
+
try {
|
|
567
|
+
const pid = resolvePid(pidSource);
|
|
568
|
+
if (pid === null) {
|
|
569
|
+
return {
|
|
570
|
+
name: "memory",
|
|
571
|
+
healthy: false,
|
|
572
|
+
score: 0,
|
|
573
|
+
message: "Gateway process not found \u2014 cannot check memory",
|
|
574
|
+
latencyMs: Date.now() - start
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const statusPath = `/proc/${pid}/status`;
|
|
578
|
+
if (!fs5.existsSync(statusPath)) {
|
|
579
|
+
return {
|
|
580
|
+
name: "memory",
|
|
581
|
+
healthy: false,
|
|
582
|
+
score: 0,
|
|
583
|
+
message: `Process ${pid} not found in /proc`,
|
|
584
|
+
latencyMs: Date.now() - start
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const status = fs5.readFileSync(statusPath, "utf-8");
|
|
588
|
+
const rssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
589
|
+
if (!rssMatch) {
|
|
590
|
+
return {
|
|
591
|
+
name: "memory",
|
|
592
|
+
healthy: true,
|
|
593
|
+
score: 1,
|
|
594
|
+
message: "Could not parse RSS from /proc status",
|
|
595
|
+
latencyMs: Date.now() - start
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const rssKb = parseInt(rssMatch[1] ?? "0", 10);
|
|
599
|
+
const rssMb = rssKb / 1024;
|
|
600
|
+
const healthy = rssMb < thresholdMb;
|
|
601
|
+
return {
|
|
602
|
+
name: "memory",
|
|
603
|
+
healthy,
|
|
604
|
+
score: healthy ? 2 : 0,
|
|
605
|
+
message: healthy ? void 0 : `RSS ${rssMb.toFixed(0)}MB exceeds threshold ${thresholdMb}MB`,
|
|
606
|
+
latencyMs: Date.now() - start
|
|
607
|
+
};
|
|
608
|
+
} catch (err) {
|
|
609
|
+
return {
|
|
610
|
+
name: "memory",
|
|
611
|
+
healthy: false,
|
|
612
|
+
score: 0,
|
|
613
|
+
message: `Memory probe error: ${err instanceof Error ? err.message : String(err)}`,
|
|
614
|
+
latencyMs: Date.now() - start
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/health/probes/cpu.ts
|
|
620
|
+
var fs6 = __toESM(require("fs"));
|
|
621
|
+
async function cpuProbe(_target, pidSource, thresholdPercent = 90, sampleMs = 1e3) {
|
|
622
|
+
const start = Date.now();
|
|
623
|
+
try {
|
|
624
|
+
const pid = resolvePid(pidSource);
|
|
625
|
+
if (pid === null) {
|
|
626
|
+
return {
|
|
627
|
+
name: "cpu",
|
|
628
|
+
healthy: false,
|
|
629
|
+
score: 0,
|
|
630
|
+
message: "Gateway process not found \u2014 cannot check CPU",
|
|
631
|
+
latencyMs: Date.now() - start
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const statPath = `/proc/${pid}/stat`;
|
|
635
|
+
if (!fs6.existsSync(statPath)) {
|
|
636
|
+
return {
|
|
637
|
+
name: "cpu",
|
|
638
|
+
healthy: false,
|
|
639
|
+
score: 0,
|
|
640
|
+
message: `Process ${pid} not found in /proc`,
|
|
641
|
+
latencyMs: Date.now() - start
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const readCpuTime = () => {
|
|
645
|
+
const stat = fs6.readFileSync(statPath, "utf-8");
|
|
646
|
+
const fields = stat.split(" ");
|
|
647
|
+
const utime = parseInt(fields[13] ?? "0", 10) || 0;
|
|
648
|
+
const stime = parseInt(fields[14] ?? "0", 10) || 0;
|
|
649
|
+
return utime + stime;
|
|
650
|
+
};
|
|
651
|
+
const cpuTime1 = readCpuTime();
|
|
652
|
+
await new Promise((r) => setTimeout(r, sampleMs));
|
|
653
|
+
const cpuTime2 = readCpuTime();
|
|
654
|
+
const clockTicks = 100;
|
|
655
|
+
const cpuDelta = (cpuTime2 - cpuTime1) / clockTicks;
|
|
656
|
+
const cpuPercent = cpuDelta / (sampleMs / 1e3) * 100;
|
|
657
|
+
const healthy = cpuPercent < thresholdPercent;
|
|
658
|
+
return {
|
|
659
|
+
name: "cpu",
|
|
660
|
+
healthy,
|
|
661
|
+
score: healthy ? 2 : 0,
|
|
662
|
+
message: healthy ? void 0 : `CPU ${cpuPercent.toFixed(1)}% exceeds threshold ${thresholdPercent}%`,
|
|
663
|
+
latencyMs: Date.now() - start
|
|
664
|
+
};
|
|
665
|
+
} catch (err) {
|
|
666
|
+
return {
|
|
667
|
+
name: "cpu",
|
|
668
|
+
healthy: false,
|
|
669
|
+
score: 0,
|
|
670
|
+
message: `CPU probe error: ${err instanceof Error ? err.message : String(err)}`,
|
|
671
|
+
latencyMs: Date.now() - start
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/health/probes/disk.ts
|
|
677
|
+
var import_node_child_process3 = require("child_process");
|
|
678
|
+
var import_node_util2 = require("util");
|
|
679
|
+
var path2 = __toESM(require("path"));
|
|
680
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
681
|
+
async function diskProbe(_target, configPath, thresholdMb = 100) {
|
|
682
|
+
const start = Date.now();
|
|
683
|
+
try {
|
|
684
|
+
const dir = path2.dirname(configPath);
|
|
685
|
+
const { stdout } = await execFileAsync2("df", ["-BM", "--output=avail", dir]);
|
|
686
|
+
const lines = stdout.trim().split("\n");
|
|
687
|
+
const lastLine = lines[lines.length - 1];
|
|
688
|
+
if (!lastLine) {
|
|
689
|
+
return {
|
|
690
|
+
name: "disk",
|
|
691
|
+
healthy: true,
|
|
692
|
+
score: 1,
|
|
693
|
+
message: "Could not parse df output",
|
|
694
|
+
latencyMs: Date.now() - start
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const availStr = lastLine.trim().replace("M", "");
|
|
698
|
+
const availMb = parseInt(availStr, 10);
|
|
699
|
+
if (isNaN(availMb)) {
|
|
700
|
+
return {
|
|
701
|
+
name: "disk",
|
|
702
|
+
healthy: true,
|
|
703
|
+
score: 1,
|
|
704
|
+
message: "Could not parse disk space",
|
|
705
|
+
latencyMs: Date.now() - start
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const healthy = availMb >= thresholdMb;
|
|
709
|
+
return {
|
|
710
|
+
name: "disk",
|
|
711
|
+
healthy,
|
|
712
|
+
score: healthy ? 2 : 0,
|
|
713
|
+
message: healthy ? void 0 : `Only ${availMb}MB available (threshold: ${thresholdMb}MB)`,
|
|
714
|
+
latencyMs: Date.now() - start
|
|
715
|
+
};
|
|
716
|
+
} catch (err) {
|
|
717
|
+
return {
|
|
718
|
+
name: "disk",
|
|
719
|
+
healthy: true,
|
|
720
|
+
score: 1,
|
|
721
|
+
message: `Disk probe fallback: ${err instanceof Error ? err.message : String(err)}`,
|
|
722
|
+
latencyMs: Date.now() - start
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/health/probes/log-tail.ts
|
|
728
|
+
var fs7 = __toESM(require("fs"));
|
|
729
|
+
var ERROR_PATTERNS = [
|
|
730
|
+
/ECONNRESET/,
|
|
731
|
+
/SIGTERM/,
|
|
732
|
+
/SIGKILL/,
|
|
733
|
+
/ENOMEM/,
|
|
734
|
+
/fatal\s+error/i,
|
|
735
|
+
/uncaught\s+exception/i,
|
|
736
|
+
/unhandled\s+rejection/i,
|
|
737
|
+
/out\s+of\s+memory/i,
|
|
738
|
+
/EACCES/,
|
|
739
|
+
/ENOSPC/
|
|
740
|
+
];
|
|
741
|
+
async function logTailProbe(_target, logPath, tailLines = 50) {
|
|
742
|
+
const start = Date.now();
|
|
743
|
+
try {
|
|
744
|
+
if (!fs7.existsSync(logPath)) {
|
|
745
|
+
return {
|
|
746
|
+
name: "logTail",
|
|
747
|
+
healthy: true,
|
|
748
|
+
score: 2,
|
|
749
|
+
message: "Log file not found (may not exist yet)",
|
|
750
|
+
latencyMs: Date.now() - start
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const content = fs7.readFileSync(logPath, "utf-8");
|
|
754
|
+
const lines = content.split("\n").slice(-tailLines);
|
|
755
|
+
const recentText = lines.join("\n");
|
|
756
|
+
const matchedPatterns = ERROR_PATTERNS.filter((p) => p.test(recentText));
|
|
757
|
+
if (matchedPatterns.length === 0) {
|
|
758
|
+
return {
|
|
759
|
+
name: "logTail",
|
|
760
|
+
healthy: true,
|
|
761
|
+
score: 2,
|
|
762
|
+
latencyMs: Date.now() - start
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const score = matchedPatterns.length >= 3 ? 0 : 1;
|
|
766
|
+
return {
|
|
767
|
+
name: "logTail",
|
|
768
|
+
healthy: false,
|
|
769
|
+
score,
|
|
770
|
+
message: `Error patterns in recent logs: ${matchedPatterns.map((p) => p.source).join(", ")}`,
|
|
771
|
+
latencyMs: Date.now() - start
|
|
772
|
+
};
|
|
773
|
+
} catch (err) {
|
|
774
|
+
return {
|
|
775
|
+
name: "logTail",
|
|
776
|
+
healthy: true,
|
|
777
|
+
score: 1,
|
|
778
|
+
message: `Log tail probe fallback: ${err instanceof Error ? err.message : String(err)}`,
|
|
779
|
+
latencyMs: Date.now() - start
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/health/probes/websocket.ts
|
|
785
|
+
var import_ws = __toESM(require("ws"));
|
|
786
|
+
async function websocketProbe(target, port, timeoutMs = 5e3) {
|
|
787
|
+
const start = Date.now();
|
|
788
|
+
const host = target.type === "remote" ? target.host : "127.0.0.1";
|
|
789
|
+
const url = `ws://${host}:${port}`;
|
|
790
|
+
return new Promise((resolve) => {
|
|
791
|
+
let resolved = false;
|
|
792
|
+
const finish = (result) => {
|
|
793
|
+
if (!resolved) {
|
|
794
|
+
resolved = true;
|
|
795
|
+
resolve(result);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const timer = setTimeout(() => {
|
|
799
|
+
ws.terminate();
|
|
800
|
+
finish({
|
|
801
|
+
name: "websocket",
|
|
802
|
+
healthy: false,
|
|
803
|
+
score: 0,
|
|
804
|
+
message: `WebSocket handshake timed out after ${timeoutMs}ms`,
|
|
805
|
+
latencyMs: Date.now() - start
|
|
806
|
+
});
|
|
807
|
+
}, timeoutMs);
|
|
808
|
+
const ws = new import_ws.default(url, { handshakeTimeout: timeoutMs });
|
|
809
|
+
ws.on("open", () => {
|
|
810
|
+
clearTimeout(timer);
|
|
811
|
+
ws.close();
|
|
812
|
+
finish({
|
|
813
|
+
name: "websocket",
|
|
814
|
+
healthy: true,
|
|
815
|
+
score: 2,
|
|
816
|
+
latencyMs: Date.now() - start
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
ws.on("error", (err) => {
|
|
820
|
+
clearTimeout(timer);
|
|
821
|
+
ws.terminate();
|
|
822
|
+
finish({
|
|
823
|
+
name: "websocket",
|
|
824
|
+
healthy: false,
|
|
825
|
+
score: 0,
|
|
826
|
+
message: `WebSocket probe failed: ${err.message}`,
|
|
827
|
+
latencyMs: Date.now() - start
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/health/monitor.ts
|
|
834
|
+
var HealthMonitor = class extends import_node_events.EventEmitter {
|
|
835
|
+
constructor(config) {
|
|
836
|
+
super();
|
|
837
|
+
this.config = config;
|
|
838
|
+
this.degradedConfirmation = new DegradedConfirmation(
|
|
839
|
+
config.monitoring.degradedConfirmationCount
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
interval = null;
|
|
843
|
+
degradedConfirmation;
|
|
844
|
+
lastScore = null;
|
|
845
|
+
async runAllProbes() {
|
|
846
|
+
const target = { type: "local" };
|
|
847
|
+
const timeout = this.config.monitoring.probeTimeoutMs;
|
|
848
|
+
const withTimeout = async (fn, name) => {
|
|
849
|
+
try {
|
|
850
|
+
return await Promise.race([
|
|
851
|
+
fn(),
|
|
852
|
+
new Promise(
|
|
853
|
+
(resolve) => setTimeout(
|
|
854
|
+
() => resolve({
|
|
855
|
+
name,
|
|
856
|
+
healthy: false,
|
|
857
|
+
score: 0,
|
|
858
|
+
message: `Probe timed out after ${timeout}ms`,
|
|
859
|
+
latencyMs: timeout
|
|
860
|
+
}),
|
|
861
|
+
timeout
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
]);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
return {
|
|
867
|
+
name,
|
|
868
|
+
healthy: false,
|
|
869
|
+
score: 0,
|
|
870
|
+
message: `Probe error: ${err instanceof Error ? err.message : String(err)}`,
|
|
871
|
+
latencyMs: 0
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
const results = await Promise.allSettled([
|
|
876
|
+
withTimeout(() => processProbe(target, this.config.gateway.pidFile), "process"),
|
|
877
|
+
withTimeout(() => portProbe(target, this.config.gateway.port, timeout), "port"),
|
|
878
|
+
withTimeout(
|
|
879
|
+
() => httpHealthProbe(
|
|
880
|
+
target,
|
|
881
|
+
this.config.gateway.port,
|
|
882
|
+
this.config.gateway.healthEndpoint,
|
|
883
|
+
timeout
|
|
884
|
+
),
|
|
885
|
+
"http"
|
|
886
|
+
),
|
|
887
|
+
withTimeout(() => configProbe(target, this.config.gateway.configPath), "config"),
|
|
888
|
+
withTimeout(() => tunProbe(target), "tun"),
|
|
889
|
+
withTimeout(
|
|
890
|
+
() => memoryProbe(target, this.config.gateway.pidFile, this.config.health.memoryThresholdMb),
|
|
891
|
+
"memory"
|
|
892
|
+
),
|
|
893
|
+
withTimeout(
|
|
894
|
+
() => cpuProbe(target, this.config.gateway.pidFile, this.config.health.cpuThresholdPercent),
|
|
895
|
+
"cpu"
|
|
896
|
+
),
|
|
897
|
+
withTimeout(
|
|
898
|
+
() => diskProbe(target, this.config.gateway.configPath, this.config.health.diskThresholdMb),
|
|
899
|
+
"disk"
|
|
900
|
+
),
|
|
901
|
+
withTimeout(() => logTailProbe(target, this.config.gateway.logPath), "logTail"),
|
|
902
|
+
withTimeout(
|
|
903
|
+
() => websocketProbe(target, this.config.gateway.port, timeout),
|
|
904
|
+
"websocket"
|
|
905
|
+
)
|
|
906
|
+
]);
|
|
907
|
+
const probeResults = results.map((r, i) => {
|
|
908
|
+
if (r.status === "fulfilled") return r.value;
|
|
909
|
+
const names = [
|
|
910
|
+
"process",
|
|
911
|
+
"port",
|
|
912
|
+
"http",
|
|
913
|
+
"config",
|
|
914
|
+
"tun",
|
|
915
|
+
"memory",
|
|
916
|
+
"cpu",
|
|
917
|
+
"disk",
|
|
918
|
+
"logTail",
|
|
919
|
+
"websocket"
|
|
920
|
+
];
|
|
921
|
+
return {
|
|
922
|
+
name: names[i] ?? "unknown",
|
|
923
|
+
healthy: false,
|
|
924
|
+
score: 0,
|
|
925
|
+
message: `Probe rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`,
|
|
926
|
+
latencyMs: 0
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
const score = computeHealthScore(probeResults, {
|
|
930
|
+
healthyMin: this.config.health.healthyMin,
|
|
931
|
+
degradedMin: this.config.health.degradedMin
|
|
932
|
+
});
|
|
933
|
+
this.lastScore = score;
|
|
934
|
+
const shouldEscalate = this.degradedConfirmation.update(score.band);
|
|
935
|
+
this.emit("check", score);
|
|
936
|
+
if (shouldEscalate && score.band !== "healthy") {
|
|
937
|
+
this.emit("escalate", score);
|
|
938
|
+
}
|
|
939
|
+
return score;
|
|
940
|
+
}
|
|
941
|
+
start() {
|
|
942
|
+
this.interval = setInterval(() => {
|
|
943
|
+
void this.runAllProbes();
|
|
944
|
+
}, this.config.monitoring.intervalMs);
|
|
945
|
+
}
|
|
946
|
+
stop() {
|
|
947
|
+
if (this.interval) {
|
|
948
|
+
clearInterval(this.interval);
|
|
949
|
+
this.interval = null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
getLastScore() {
|
|
953
|
+
return this.lastScore;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/cli/commands/status.ts
|
|
958
|
+
var statusCommand = new import_commander.Command("status").description("Show current health status and recovery stats").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).action(async (opts) => {
|
|
959
|
+
const config = loadConfig(opts.config);
|
|
960
|
+
const monitor = new HealthMonitor(config);
|
|
961
|
+
const score = await monitor.runAllProbes();
|
|
962
|
+
const bandColors = {
|
|
963
|
+
healthy: "\x1B[32m",
|
|
964
|
+
degraded: "\x1B[33m",
|
|
965
|
+
critical: "\x1B[31m"
|
|
966
|
+
};
|
|
967
|
+
const reset = "\x1B[0m";
|
|
968
|
+
const color = bandColors[score.band] ?? "";
|
|
969
|
+
process.stdout.write(`
|
|
970
|
+
Health: ${color}${score.band.toUpperCase()}${reset} (score: ${score.total})
|
|
971
|
+
|
|
972
|
+
`);
|
|
973
|
+
for (const probe of score.probeResults) {
|
|
974
|
+
const icon = probe.healthy ? "\x1B[32m+\x1B[0m" : "\x1B[31m-\x1B[0m";
|
|
975
|
+
const msg = probe.message ? ` \u2014 ${probe.message}` : "";
|
|
976
|
+
process.stdout.write(` ${icon} ${probe.name} (${probe.latencyMs}ms)${msg}
|
|
977
|
+
`);
|
|
978
|
+
}
|
|
979
|
+
if (config.alerts.channels.length === 0) {
|
|
980
|
+
process.stdout.write(
|
|
981
|
+
`
|
|
982
|
+
\x1B[33mWARNING: No alert channels configured. Aegis cannot notify you during incidents. Run 'aegis init' to add alerts.\x1B[0m
|
|
983
|
+
`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
process.stdout.write("\n");
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// src/cli/commands/check.ts
|
|
990
|
+
var import_commander2 = require("commander");
|
|
991
|
+
var checkCommand = new import_commander2.Command("check").description("Run all health probes once and exit").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).option("--json", "Output as JSON").action(async (opts) => {
|
|
992
|
+
const config = loadConfig(opts.config);
|
|
993
|
+
const monitor = new HealthMonitor(config);
|
|
994
|
+
const score = await monitor.runAllProbes();
|
|
995
|
+
if (opts.json) {
|
|
996
|
+
process.stdout.write(JSON.stringify(score, null, 2) + "\n");
|
|
997
|
+
} else {
|
|
998
|
+
const failed = score.probeResults.filter((p) => !p.healthy);
|
|
999
|
+
process.stdout.write(`Health: ${score.band.toUpperCase()} (score: ${score.total})
|
|
1000
|
+
`);
|
|
1001
|
+
process.stdout.write(`Probes: ${score.probeResults.length - failed.length} passed, ${failed.length} failed
|
|
1002
|
+
`);
|
|
1003
|
+
if (failed.length > 0) {
|
|
1004
|
+
process.stdout.write("\nFailed probes:\n");
|
|
1005
|
+
for (const probe of failed) {
|
|
1006
|
+
process.stdout.write(` - ${probe.name}: ${probe.message ?? "failed"}
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
process.exit(score.band === "healthy" ? 0 : 1);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// src/cli/commands/init.ts
|
|
1015
|
+
var import_commander3 = require("commander");
|
|
1016
|
+
var fs8 = __toESM(require("fs"));
|
|
1017
|
+
var readline = __toESM(require("readline"));
|
|
1018
|
+
function ask(rl, question, defaultValue) {
|
|
1019
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
1020
|
+
return new Promise((resolve) => {
|
|
1021
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
1022
|
+
resolve(answer.trim() || defaultValue || "");
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
function detectPort() {
|
|
1027
|
+
try {
|
|
1028
|
+
const configPath = expandHome("~/.openclaw/openclaw.json");
|
|
1029
|
+
if (fs8.existsSync(configPath)) {
|
|
1030
|
+
const raw = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
1031
|
+
if (raw?.gateway?.port) return raw.gateway.port;
|
|
1032
|
+
}
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
return 3e3;
|
|
1036
|
+
}
|
|
1037
|
+
function generateToml(opts) {
|
|
1038
|
+
let toml = `# OpenClaw Aegis Configuration
|
|
1039
|
+
# Generated by 'aegis init'
|
|
1040
|
+
|
|
1041
|
+
[gateway]
|
|
1042
|
+
configPath = "~/.openclaw/openclaw.json"
|
|
1043
|
+
pidFile = "openclaw-gateway.service"
|
|
1044
|
+
port = ${opts.port}
|
|
1045
|
+
logPath = "~/.openclaw/logs/gateway.log"
|
|
1046
|
+
healthEndpoint = "/health"
|
|
1047
|
+
|
|
1048
|
+
[monitoring]
|
|
1049
|
+
intervalMs = 10000
|
|
1050
|
+
probeTimeoutMs = 5000
|
|
1051
|
+
|
|
1052
|
+
[health]
|
|
1053
|
+
memoryThresholdMb = ${opts.memoryThresholdMb}
|
|
1054
|
+
cpuThresholdPercent = 90
|
|
1055
|
+
diskThresholdMb = 100
|
|
1056
|
+
|
|
1057
|
+
[recovery]
|
|
1058
|
+
l1MaxAttempts = 3
|
|
1059
|
+
l2MaxAttempts = 2
|
|
1060
|
+
|
|
1061
|
+
[backup]
|
|
1062
|
+
maxKnownGood = 5
|
|
1063
|
+
knownGoodStabilityMs = 60000
|
|
1064
|
+
|
|
1065
|
+
[deadManSwitch]
|
|
1066
|
+
enabled = true
|
|
1067
|
+
countdownMs = 30000
|
|
1068
|
+
|
|
1069
|
+
[platform]
|
|
1070
|
+
type = "systemd"
|
|
1071
|
+
`;
|
|
1072
|
+
if (opts.channels.length > 0) {
|
|
1073
|
+
toml += "\n[alerts]\n";
|
|
1074
|
+
for (const ch of opts.channels) {
|
|
1075
|
+
toml += "\n[[alerts.channels]]\n";
|
|
1076
|
+
for (const [key, val] of Object.entries(ch)) {
|
|
1077
|
+
if (typeof val === "number") {
|
|
1078
|
+
toml += `${key} = ${val}
|
|
1079
|
+
`;
|
|
1080
|
+
} else {
|
|
1081
|
+
toml += `${key} = "${val}"
|
|
1082
|
+
`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return toml;
|
|
1088
|
+
}
|
|
1089
|
+
var initCommand = new import_commander3.Command("init").description("Interactive setup wizard \u2014 configure Aegis for your gateway").option("--auto", "Auto-detect everything, no prompts").action(async (opts) => {
|
|
1090
|
+
const configDir = getConfigDir();
|
|
1091
|
+
const configFile = expandHome(DEFAULT_CONFIG_PATH);
|
|
1092
|
+
if (fs8.existsSync(configFile) && !opts.auto) {
|
|
1093
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1094
|
+
const overwrite = await ask(rl, "Config already exists. Overwrite? (y/N)", "n");
|
|
1095
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
1096
|
+
console.log("Aborted.");
|
|
1097
|
+
rl.close();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
rl.close();
|
|
1101
|
+
}
|
|
1102
|
+
const detectedPort = detectPort();
|
|
1103
|
+
const channels = [];
|
|
1104
|
+
if (opts.auto) {
|
|
1105
|
+
console.log("Auto-detecting configuration...");
|
|
1106
|
+
console.log(` Gateway port: ${detectedPort}`);
|
|
1107
|
+
console.log(` PID source: openclaw-gateway.service (systemd)`);
|
|
1108
|
+
console.log(` Memory threshold: 768MB`);
|
|
1109
|
+
} else {
|
|
1110
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1111
|
+
console.log("\n OpenClaw Aegis \u2014 Setup Wizard\n");
|
|
1112
|
+
console.log(" Detected gateway config at ~/.openclaw/openclaw.json");
|
|
1113
|
+
console.log(` Detected gateway port: ${detectedPort}
|
|
1114
|
+
`);
|
|
1115
|
+
const portStr = await ask(rl, "Gateway port", String(detectedPort));
|
|
1116
|
+
const port = parseInt(portStr, 10) || detectedPort;
|
|
1117
|
+
const memStr = await ask(rl, "Memory threshold (MB)", "768");
|
|
1118
|
+
const memThreshold = parseInt(memStr, 10) || 768;
|
|
1119
|
+
console.log("\n Alert Channels (out-of-band \u2014 never through the gateway)\n");
|
|
1120
|
+
console.log(" Supported: ntfy, telegram, whatsapp, slack, discord, email, pushover, webhook\n");
|
|
1121
|
+
let addMore = true;
|
|
1122
|
+
while (addMore) {
|
|
1123
|
+
const channelType = await ask(rl, "Add alert channel? (ntfy/telegram/whatsapp/slack/discord/email/pushover/webhook/skip)", "skip");
|
|
1124
|
+
if (channelType === "skip" || channelType === "") {
|
|
1125
|
+
addMore = false;
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
switch (channelType) {
|
|
1129
|
+
case "ntfy": {
|
|
1130
|
+
const topic = await ask(rl, " ntfy topic", "aegis-alerts");
|
|
1131
|
+
const url = await ask(rl, " ntfy server URL", "https://ntfy.sh");
|
|
1132
|
+
channels.push({ type: "ntfy", topic, url, priority: 4 });
|
|
1133
|
+
console.log(` Added ntfy channel (${url}/${topic})
|
|
1134
|
+
`);
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
case "telegram": {
|
|
1138
|
+
const botToken = await ask(rl, " Telegram bot token");
|
|
1139
|
+
const chatId = await ask(rl, " Telegram chat ID");
|
|
1140
|
+
if (botToken && chatId) {
|
|
1141
|
+
channels.push({ type: "telegram", botToken, chatId });
|
|
1142
|
+
console.log(" Added Telegram channel\n");
|
|
1143
|
+
} else {
|
|
1144
|
+
console.log(" Skipped \u2014 bot token and chat ID required\n");
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
case "whatsapp": {
|
|
1149
|
+
const phoneNumberId = await ask(rl, " WhatsApp Business phone number ID");
|
|
1150
|
+
const accessToken = await ask(rl, " WhatsApp Cloud API access token");
|
|
1151
|
+
const recipientNumber = await ask(rl, " Recipient phone number (with country code, e.g. 61412345678)");
|
|
1152
|
+
if (phoneNumberId && accessToken && recipientNumber) {
|
|
1153
|
+
channels.push({ type: "whatsapp", phoneNumberId, accessToken, recipientNumber });
|
|
1154
|
+
console.log(" Added WhatsApp channel\n");
|
|
1155
|
+
} else {
|
|
1156
|
+
console.log(" Skipped \u2014 all three fields required\n");
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case "slack": {
|
|
1161
|
+
const webhookUrl = await ask(rl, " Slack Incoming Webhook URL");
|
|
1162
|
+
if (webhookUrl) {
|
|
1163
|
+
const channel = await ask(rl, " Slack channel override (optional, e.g. #alerts)");
|
|
1164
|
+
const ch = { type: "slack", webhookUrl };
|
|
1165
|
+
if (channel) ch.channel = channel;
|
|
1166
|
+
channels.push(ch);
|
|
1167
|
+
console.log(" Added Slack channel\n");
|
|
1168
|
+
} else {
|
|
1169
|
+
console.log(" Skipped \u2014 webhook URL required\n");
|
|
1170
|
+
}
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
case "discord": {
|
|
1174
|
+
const webhookUrl = await ask(rl, " Discord Webhook URL");
|
|
1175
|
+
if (webhookUrl) {
|
|
1176
|
+
const username = await ask(rl, " Bot display name (optional)", "Aegis");
|
|
1177
|
+
const ch = { type: "discord", webhookUrl };
|
|
1178
|
+
if (username && username !== "Aegis") ch.username = username;
|
|
1179
|
+
channels.push(ch);
|
|
1180
|
+
console.log(" Added Discord channel\n");
|
|
1181
|
+
} else {
|
|
1182
|
+
console.log(" Skipped \u2014 webhook URL required\n");
|
|
1183
|
+
}
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
case "email": {
|
|
1187
|
+
const host = await ask(rl, " SMTP host (e.g. smtp.gmail.com)");
|
|
1188
|
+
const portStr2 = await ask(rl, " SMTP port", "587");
|
|
1189
|
+
const username = await ask(rl, " SMTP username");
|
|
1190
|
+
const password = await ask(rl, " SMTP password");
|
|
1191
|
+
const from = await ask(rl, " From address");
|
|
1192
|
+
const to = await ask(rl, " To address");
|
|
1193
|
+
if (host && username && password && from && to) {
|
|
1194
|
+
channels.push({
|
|
1195
|
+
type: "email",
|
|
1196
|
+
host,
|
|
1197
|
+
port: parseInt(portStr2, 10) || 587,
|
|
1198
|
+
secure: 0,
|
|
1199
|
+
username,
|
|
1200
|
+
password,
|
|
1201
|
+
from,
|
|
1202
|
+
to
|
|
1203
|
+
});
|
|
1204
|
+
console.log(" Added Email channel\n");
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log(" Skipped \u2014 all fields required\n");
|
|
1207
|
+
}
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
case "pushover": {
|
|
1211
|
+
const apiToken = await ask(rl, " Pushover API token (from your app)");
|
|
1212
|
+
const userKey = await ask(rl, " Pushover user key");
|
|
1213
|
+
if (apiToken && userKey) {
|
|
1214
|
+
const device = await ask(rl, " Device name (optional)");
|
|
1215
|
+
const ch = { type: "pushover", apiToken, userKey };
|
|
1216
|
+
if (device) ch.device = device;
|
|
1217
|
+
channels.push(ch);
|
|
1218
|
+
console.log(" Added Pushover channel\n");
|
|
1219
|
+
} else {
|
|
1220
|
+
console.log(" Skipped \u2014 API token and user key required\n");
|
|
1221
|
+
}
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
case "webhook": {
|
|
1225
|
+
const url = await ask(rl, " Webhook URL");
|
|
1226
|
+
if (url) {
|
|
1227
|
+
const secret = await ask(rl, " Webhook secret (optional)");
|
|
1228
|
+
const ch = { type: "webhook", url };
|
|
1229
|
+
if (secret) ch.secret = secret;
|
|
1230
|
+
channels.push(ch);
|
|
1231
|
+
console.log(` Added webhook channel (${url})
|
|
1232
|
+
`);
|
|
1233
|
+
} else {
|
|
1234
|
+
console.log(" Skipped \u2014 URL required\n");
|
|
1235
|
+
}
|
|
1236
|
+
break;
|
|
1237
|
+
}
|
|
1238
|
+
default:
|
|
1239
|
+
console.log(` Unknown channel type: ${channelType}
|
|
1240
|
+
`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
rl.close();
|
|
1244
|
+
const toml2 = generateToml({ port, memoryThresholdMb: memThreshold, channels });
|
|
1245
|
+
ensureConfigDir();
|
|
1246
|
+
fs8.writeFileSync(configFile, toml2, { mode: 384 });
|
|
1247
|
+
console.log(`
|
|
1248
|
+
Config written to ${configFile}`);
|
|
1249
|
+
console.log("\n Running health check...\n");
|
|
1250
|
+
const config2 = loadConfig(configFile);
|
|
1251
|
+
const monitor2 = new HealthMonitor(config2);
|
|
1252
|
+
const score2 = await monitor2.runAllProbes();
|
|
1253
|
+
const passed2 = score2.probeResults.filter((p) => p.healthy).length;
|
|
1254
|
+
const failed = score2.probeResults.filter((p) => !p.healthy);
|
|
1255
|
+
console.log(` Health: ${score2.band.toUpperCase()} (${passed2}/${score2.probeResults.length} probes passed)`);
|
|
1256
|
+
if (failed.length > 0) {
|
|
1257
|
+
for (const probe of failed) {
|
|
1258
|
+
console.log(` - ${probe.name}: ${probe.message ?? "failed"}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (channels.length === 0) {
|
|
1262
|
+
console.log("\n No alert channels configured. You can add them later by editing:");
|
|
1263
|
+
console.log(` ${configFile}`);
|
|
1264
|
+
}
|
|
1265
|
+
console.log("\n Setup complete. Run 'aegis check' to verify anytime.\n");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const toml = generateToml({ port: detectedPort, memoryThresholdMb: 768, channels });
|
|
1269
|
+
ensureConfigDir();
|
|
1270
|
+
fs8.writeFileSync(configFile, toml, { mode: 384 });
|
|
1271
|
+
console.log(`Config written to ${configFile}`);
|
|
1272
|
+
const config = loadConfig(configFile);
|
|
1273
|
+
const monitor = new HealthMonitor(config);
|
|
1274
|
+
const score = await monitor.runAllProbes();
|
|
1275
|
+
const passed = score.probeResults.filter((p) => p.healthy).length;
|
|
1276
|
+
console.log(`Health: ${score.band.toUpperCase()} (${passed}/${score.probeResults.length} probes passed)`);
|
|
1277
|
+
console.log("\nSetup complete.");
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// src/cli/commands/test-alert.ts
|
|
1281
|
+
var import_commander4 = require("commander");
|
|
1282
|
+
|
|
1283
|
+
// src/alerts/dispatcher.ts
|
|
1284
|
+
var AlertDispatcher = class {
|
|
1285
|
+
providers = [];
|
|
1286
|
+
retryBackoffMs;
|
|
1287
|
+
retryAttempts;
|
|
1288
|
+
constructor(retryAttempts = 3, retryBackoffMs = [5e3, 15e3, 45e3]) {
|
|
1289
|
+
this.retryAttempts = retryAttempts;
|
|
1290
|
+
this.retryBackoffMs = retryBackoffMs;
|
|
1291
|
+
}
|
|
1292
|
+
addProvider(provider) {
|
|
1293
|
+
this.providers.push(provider);
|
|
1294
|
+
}
|
|
1295
|
+
getProviders() {
|
|
1296
|
+
return [...this.providers];
|
|
1297
|
+
}
|
|
1298
|
+
hasProviders() {
|
|
1299
|
+
return this.providers.length > 0;
|
|
1300
|
+
}
|
|
1301
|
+
async dispatch(alert) {
|
|
1302
|
+
if (this.providers.length === 0) {
|
|
1303
|
+
return { sent: false, results: [], allFailed: true };
|
|
1304
|
+
}
|
|
1305
|
+
const scrubbed = scrubSensitiveData(alert);
|
|
1306
|
+
const results = [];
|
|
1307
|
+
for (const provider of this.providers) {
|
|
1308
|
+
const result = await this.sendWithRetry(provider, scrubbed);
|
|
1309
|
+
results.push(result);
|
|
1310
|
+
}
|
|
1311
|
+
const anySuccess = results.some((r) => r.success);
|
|
1312
|
+
return { sent: anySuccess, results, allFailed: !anySuccess };
|
|
1313
|
+
}
|
|
1314
|
+
async testAll() {
|
|
1315
|
+
const results = /* @__PURE__ */ new Map();
|
|
1316
|
+
for (const provider of this.providers) {
|
|
1317
|
+
try {
|
|
1318
|
+
const ok = await provider.test();
|
|
1319
|
+
results.set(provider.name, ok);
|
|
1320
|
+
} catch {
|
|
1321
|
+
results.set(provider.name, false);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return results;
|
|
1325
|
+
}
|
|
1326
|
+
async sendWithRetry(provider, alert) {
|
|
1327
|
+
let lastResult = {
|
|
1328
|
+
provider: provider.name,
|
|
1329
|
+
success: false,
|
|
1330
|
+
error: "No attempts made",
|
|
1331
|
+
durationMs: 0
|
|
1332
|
+
};
|
|
1333
|
+
for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
|
|
1334
|
+
try {
|
|
1335
|
+
lastResult = await provider.send(alert);
|
|
1336
|
+
if (lastResult.success) return lastResult;
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
lastResult = {
|
|
1339
|
+
provider: provider.name,
|
|
1340
|
+
success: false,
|
|
1341
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1342
|
+
durationMs: 0
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (attempt < this.retryAttempts) {
|
|
1346
|
+
const delay = this.retryBackoffMs[attempt] ?? this.retryBackoffMs[this.retryBackoffMs.length - 1] ?? 5e3;
|
|
1347
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return lastResult;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
function scrubSensitiveData(alert) {
|
|
1354
|
+
return {
|
|
1355
|
+
...alert,
|
|
1356
|
+
body: scrubString(alert.body),
|
|
1357
|
+
title: scrubString(alert.title)
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
function scrubString(input) {
|
|
1361
|
+
return input.replace(
|
|
1362
|
+
/("[^"]*(?:key|secret|token|password|credential|auth)[^"]*"\s*:\s*)"[^"]*"/gi,
|
|
1363
|
+
'$1"[REDACTED]"'
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/alerts/providers/ntfy.ts
|
|
1368
|
+
var NtfyProvider = class {
|
|
1369
|
+
name = "ntfy";
|
|
1370
|
+
config;
|
|
1371
|
+
constructor(config) {
|
|
1372
|
+
this.config = config;
|
|
1373
|
+
}
|
|
1374
|
+
async send(alert) {
|
|
1375
|
+
const start = Date.now();
|
|
1376
|
+
const url = `${this.config.url}/${this.config.topic}`;
|
|
1377
|
+
try {
|
|
1378
|
+
const response = await fetch(url, {
|
|
1379
|
+
method: "POST",
|
|
1380
|
+
headers: {
|
|
1381
|
+
"Title": alert.title,
|
|
1382
|
+
"Priority": String(this.config.priority),
|
|
1383
|
+
"Tags": alertSeverityToTag(alert.severity)
|
|
1384
|
+
},
|
|
1385
|
+
body: alert.body
|
|
1386
|
+
});
|
|
1387
|
+
return {
|
|
1388
|
+
provider: this.name,
|
|
1389
|
+
success: response.ok,
|
|
1390
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`,
|
|
1391
|
+
durationMs: Date.now() - start
|
|
1392
|
+
};
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
return {
|
|
1395
|
+
provider: this.name,
|
|
1396
|
+
success: false,
|
|
1397
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1398
|
+
durationMs: Date.now() - start
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async test() {
|
|
1403
|
+
try {
|
|
1404
|
+
const result = await this.send({
|
|
1405
|
+
severity: "info",
|
|
1406
|
+
title: "Aegis Alert Test",
|
|
1407
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1408
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1409
|
+
});
|
|
1410
|
+
return result.success;
|
|
1411
|
+
} catch {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
function alertSeverityToTag(severity) {
|
|
1417
|
+
switch (severity) {
|
|
1418
|
+
case "critical":
|
|
1419
|
+
return "rotating_light";
|
|
1420
|
+
case "warning":
|
|
1421
|
+
return "warning";
|
|
1422
|
+
default:
|
|
1423
|
+
return "information_source";
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/alerts/providers/telegram.ts
|
|
1428
|
+
var TelegramProvider = class {
|
|
1429
|
+
name = "telegram";
|
|
1430
|
+
config;
|
|
1431
|
+
constructor(config) {
|
|
1432
|
+
this.config = config;
|
|
1433
|
+
}
|
|
1434
|
+
async send(alert) {
|
|
1435
|
+
const start = Date.now();
|
|
1436
|
+
const url = `https://api.telegram.org/bot${this.config.botToken}/sendMessage`;
|
|
1437
|
+
const icon = alert.severity === "critical" ? "\u{1F6A8}" : alert.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
1438
|
+
const text = `${icon} *${escapeMarkdown(alert.title)}*
|
|
1439
|
+
|
|
1440
|
+
${escapeMarkdown(alert.body)}`;
|
|
1441
|
+
try {
|
|
1442
|
+
const response = await fetch(url, {
|
|
1443
|
+
method: "POST",
|
|
1444
|
+
headers: { "Content-Type": "application/json" },
|
|
1445
|
+
body: JSON.stringify({
|
|
1446
|
+
chat_id: this.config.chatId,
|
|
1447
|
+
text,
|
|
1448
|
+
parse_mode: "MarkdownV2"
|
|
1449
|
+
})
|
|
1450
|
+
});
|
|
1451
|
+
const data = await response.json();
|
|
1452
|
+
return {
|
|
1453
|
+
provider: this.name,
|
|
1454
|
+
success: data.ok,
|
|
1455
|
+
error: data.ok ? void 0 : data.description,
|
|
1456
|
+
durationMs: Date.now() - start
|
|
1457
|
+
};
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
return {
|
|
1460
|
+
provider: this.name,
|
|
1461
|
+
success: false,
|
|
1462
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1463
|
+
durationMs: Date.now() - start
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async test() {
|
|
1468
|
+
try {
|
|
1469
|
+
const result = await this.send({
|
|
1470
|
+
severity: "info",
|
|
1471
|
+
title: "Aegis Alert Test",
|
|
1472
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1473
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1474
|
+
});
|
|
1475
|
+
return result.success;
|
|
1476
|
+
} catch {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
function escapeMarkdown(text) {
|
|
1482
|
+
return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/alerts/providers/whatsapp.ts
|
|
1486
|
+
var WhatsAppProvider = class {
|
|
1487
|
+
name = "whatsapp";
|
|
1488
|
+
config;
|
|
1489
|
+
constructor(config) {
|
|
1490
|
+
this.config = config;
|
|
1491
|
+
}
|
|
1492
|
+
async send(alert) {
|
|
1493
|
+
const start = Date.now();
|
|
1494
|
+
const url = `https://graph.facebook.com/v21.0/${this.config.phoneNumberId}/messages`;
|
|
1495
|
+
const icon = alert.severity === "critical" ? "\u{1F6A8}" : alert.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
1496
|
+
const text = `${icon} *${alert.title}*
|
|
1497
|
+
|
|
1498
|
+
${alert.body}`;
|
|
1499
|
+
try {
|
|
1500
|
+
const response = await fetch(url, {
|
|
1501
|
+
method: "POST",
|
|
1502
|
+
headers: {
|
|
1503
|
+
"Authorization": `Bearer ${this.config.accessToken}`,
|
|
1504
|
+
"Content-Type": "application/json"
|
|
1505
|
+
},
|
|
1506
|
+
body: JSON.stringify({
|
|
1507
|
+
messaging_product: "whatsapp",
|
|
1508
|
+
to: this.config.recipientNumber,
|
|
1509
|
+
type: "text",
|
|
1510
|
+
text: { body: text }
|
|
1511
|
+
})
|
|
1512
|
+
});
|
|
1513
|
+
const data = await response.json();
|
|
1514
|
+
const success = response.ok && !!data.messages?.length;
|
|
1515
|
+
return {
|
|
1516
|
+
provider: this.name,
|
|
1517
|
+
success,
|
|
1518
|
+
error: success ? void 0 : data.error?.message ?? `HTTP ${response.status}`,
|
|
1519
|
+
durationMs: Date.now() - start
|
|
1520
|
+
};
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
return {
|
|
1523
|
+
provider: this.name,
|
|
1524
|
+
success: false,
|
|
1525
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1526
|
+
durationMs: Date.now() - start
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async test() {
|
|
1531
|
+
try {
|
|
1532
|
+
const result = await this.send({
|
|
1533
|
+
severity: "info",
|
|
1534
|
+
title: "Aegis Alert Test",
|
|
1535
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1536
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1537
|
+
});
|
|
1538
|
+
return result.success;
|
|
1539
|
+
} catch {
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
// src/alerts/providers/webhook.ts
|
|
1546
|
+
var crypto = __toESM(require("crypto"));
|
|
1547
|
+
var WebhookProvider = class {
|
|
1548
|
+
name = "webhook";
|
|
1549
|
+
config;
|
|
1550
|
+
constructor(config) {
|
|
1551
|
+
this.config = config;
|
|
1552
|
+
}
|
|
1553
|
+
async send(alert) {
|
|
1554
|
+
const start = Date.now();
|
|
1555
|
+
const body = JSON.stringify(alert);
|
|
1556
|
+
const headers = { "Content-Type": "application/json" };
|
|
1557
|
+
if (this.config.secret) {
|
|
1558
|
+
const signature = crypto.createHmac("sha256", this.config.secret).update(body).digest("hex");
|
|
1559
|
+
headers["X-Aegis-Signature"] = `sha256=${signature}`;
|
|
1560
|
+
}
|
|
1561
|
+
try {
|
|
1562
|
+
const response = await fetch(this.config.url, {
|
|
1563
|
+
method: "POST",
|
|
1564
|
+
headers,
|
|
1565
|
+
body
|
|
1566
|
+
});
|
|
1567
|
+
return {
|
|
1568
|
+
provider: this.name,
|
|
1569
|
+
success: response.ok,
|
|
1570
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`,
|
|
1571
|
+
durationMs: Date.now() - start
|
|
1572
|
+
};
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
return {
|
|
1575
|
+
provider: this.name,
|
|
1576
|
+
success: false,
|
|
1577
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1578
|
+
durationMs: Date.now() - start
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async test() {
|
|
1583
|
+
try {
|
|
1584
|
+
const result = await this.send({
|
|
1585
|
+
severity: "info",
|
|
1586
|
+
title: "Aegis Webhook Test",
|
|
1587
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1588
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1589
|
+
});
|
|
1590
|
+
return result.success;
|
|
1591
|
+
} catch {
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
// src/alerts/providers/slack.ts
|
|
1598
|
+
var SlackProvider = class {
|
|
1599
|
+
name = "slack";
|
|
1600
|
+
config;
|
|
1601
|
+
constructor(config) {
|
|
1602
|
+
this.config = config;
|
|
1603
|
+
}
|
|
1604
|
+
async send(alert) {
|
|
1605
|
+
const start = Date.now();
|
|
1606
|
+
const icon = alert.severity === "critical" ? ":rotating_light:" : alert.severity === "warning" ? ":warning:" : ":information_source:";
|
|
1607
|
+
const payload = {
|
|
1608
|
+
text: `${icon} *${alert.title}*
|
|
1609
|
+
|
|
1610
|
+
${alert.body}`
|
|
1611
|
+
};
|
|
1612
|
+
if (this.config.channel) {
|
|
1613
|
+
payload.channel = this.config.channel;
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
1617
|
+
method: "POST",
|
|
1618
|
+
headers: { "Content-Type": "application/json" },
|
|
1619
|
+
body: JSON.stringify(payload)
|
|
1620
|
+
});
|
|
1621
|
+
const ok = response.ok;
|
|
1622
|
+
return {
|
|
1623
|
+
provider: this.name,
|
|
1624
|
+
success: ok,
|
|
1625
|
+
error: ok ? void 0 : `HTTP ${response.status}`,
|
|
1626
|
+
durationMs: Date.now() - start
|
|
1627
|
+
};
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
return {
|
|
1630
|
+
provider: this.name,
|
|
1631
|
+
success: false,
|
|
1632
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1633
|
+
durationMs: Date.now() - start
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async test() {
|
|
1638
|
+
try {
|
|
1639
|
+
const result = await this.send({
|
|
1640
|
+
severity: "info",
|
|
1641
|
+
title: "Aegis Alert Test",
|
|
1642
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1643
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1644
|
+
});
|
|
1645
|
+
return result.success;
|
|
1646
|
+
} catch {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// src/alerts/providers/discord.ts
|
|
1653
|
+
var DiscordProvider = class {
|
|
1654
|
+
name = "discord";
|
|
1655
|
+
config;
|
|
1656
|
+
constructor(config) {
|
|
1657
|
+
this.config = config;
|
|
1658
|
+
}
|
|
1659
|
+
async send(alert) {
|
|
1660
|
+
const start = Date.now();
|
|
1661
|
+
const color = alert.severity === "critical" ? 15548997 : alert.severity === "warning" ? 16705372 : 5763719;
|
|
1662
|
+
const payload = {
|
|
1663
|
+
embeds: [{
|
|
1664
|
+
title: alert.title,
|
|
1665
|
+
description: alert.body,
|
|
1666
|
+
color,
|
|
1667
|
+
timestamp: alert.timestamp,
|
|
1668
|
+
footer: { text: "OpenClaw Aegis" }
|
|
1669
|
+
}]
|
|
1670
|
+
};
|
|
1671
|
+
if (this.config.username) {
|
|
1672
|
+
payload.username = this.config.username;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
1676
|
+
method: "POST",
|
|
1677
|
+
headers: { "Content-Type": "application/json" },
|
|
1678
|
+
body: JSON.stringify(payload)
|
|
1679
|
+
});
|
|
1680
|
+
const ok = response.status === 204 || response.ok;
|
|
1681
|
+
return {
|
|
1682
|
+
provider: this.name,
|
|
1683
|
+
success: ok,
|
|
1684
|
+
error: ok ? void 0 : `HTTP ${response.status}`,
|
|
1685
|
+
durationMs: Date.now() - start
|
|
1686
|
+
};
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
return {
|
|
1689
|
+
provider: this.name,
|
|
1690
|
+
success: false,
|
|
1691
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1692
|
+
durationMs: Date.now() - start
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
async test() {
|
|
1697
|
+
try {
|
|
1698
|
+
const result = await this.send({
|
|
1699
|
+
severity: "info",
|
|
1700
|
+
title: "Aegis Alert Test",
|
|
1701
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1702
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1703
|
+
});
|
|
1704
|
+
return result.success;
|
|
1705
|
+
} catch {
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// src/alerts/providers/email.ts
|
|
1712
|
+
var net2 = __toESM(require("net"));
|
|
1713
|
+
var tls = __toESM(require("tls"));
|
|
1714
|
+
var EmailProvider = class {
|
|
1715
|
+
name = "email";
|
|
1716
|
+
config;
|
|
1717
|
+
constructor(config) {
|
|
1718
|
+
this.config = config;
|
|
1719
|
+
}
|
|
1720
|
+
async send(alert) {
|
|
1721
|
+
const start = Date.now();
|
|
1722
|
+
const icon = alert.severity === "critical" ? "[CRITICAL]" : alert.severity === "warning" ? "[WARNING]" : "[INFO]";
|
|
1723
|
+
const subject = `${icon} ${alert.title}`;
|
|
1724
|
+
try {
|
|
1725
|
+
await this.sendSmtp(subject, alert.body);
|
|
1726
|
+
return {
|
|
1727
|
+
provider: this.name,
|
|
1728
|
+
success: true,
|
|
1729
|
+
durationMs: Date.now() - start
|
|
1730
|
+
};
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
return {
|
|
1733
|
+
provider: this.name,
|
|
1734
|
+
success: false,
|
|
1735
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1736
|
+
durationMs: Date.now() - start
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async test() {
|
|
1741
|
+
try {
|
|
1742
|
+
const result = await this.send({
|
|
1743
|
+
severity: "info",
|
|
1744
|
+
title: "Aegis Alert Test",
|
|
1745
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1746
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1747
|
+
});
|
|
1748
|
+
return result.success;
|
|
1749
|
+
} catch {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
sendSmtp(subject, body) {
|
|
1754
|
+
return new Promise((resolve, reject) => {
|
|
1755
|
+
const timeout = 15e3;
|
|
1756
|
+
let socket;
|
|
1757
|
+
const connect2 = () => {
|
|
1758
|
+
if (this.config.secure) {
|
|
1759
|
+
socket = tls.connect({
|
|
1760
|
+
host: this.config.host,
|
|
1761
|
+
port: this.config.port,
|
|
1762
|
+
rejectUnauthorized: true
|
|
1763
|
+
});
|
|
1764
|
+
} else {
|
|
1765
|
+
socket = net2.createConnection({
|
|
1766
|
+
host: this.config.host,
|
|
1767
|
+
port: this.config.port
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
socket.setTimeout(timeout);
|
|
1771
|
+
let buffer = "";
|
|
1772
|
+
let step = 0;
|
|
1773
|
+
const commands = [
|
|
1774
|
+
`EHLO aegis\r
|
|
1775
|
+
`,
|
|
1776
|
+
`AUTH LOGIN\r
|
|
1777
|
+
`,
|
|
1778
|
+
`${Buffer.from(this.config.username).toString("base64")}\r
|
|
1779
|
+
`,
|
|
1780
|
+
`${Buffer.from(this.config.password).toString("base64")}\r
|
|
1781
|
+
`,
|
|
1782
|
+
`MAIL FROM:<${this.config.from}>\r
|
|
1783
|
+
`,
|
|
1784
|
+
`RCPT TO:<${this.config.to}>\r
|
|
1785
|
+
`,
|
|
1786
|
+
`DATA\r
|
|
1787
|
+
`,
|
|
1788
|
+
`From: ${this.config.from}\r
|
|
1789
|
+
To: ${this.config.to}\r
|
|
1790
|
+
Subject: ${subject}\r
|
|
1791
|
+
Content-Type: text/plain; charset=utf-8\r
|
|
1792
|
+
X-Mailer: OpenClaw-Aegis\r
|
|
1793
|
+
\r
|
|
1794
|
+
${body}\r
|
|
1795
|
+
.\r
|
|
1796
|
+
`,
|
|
1797
|
+
`QUIT\r
|
|
1798
|
+
`
|
|
1799
|
+
];
|
|
1800
|
+
socket.on("data", (data) => {
|
|
1801
|
+
buffer += data.toString();
|
|
1802
|
+
const lines = buffer.split("\r\n");
|
|
1803
|
+
buffer = lines.pop() ?? "";
|
|
1804
|
+
for (const line of lines) {
|
|
1805
|
+
if (!line) continue;
|
|
1806
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
1807
|
+
if (line.charAt(3) === "-") continue;
|
|
1808
|
+
if (code >= 400) {
|
|
1809
|
+
socket.destroy();
|
|
1810
|
+
reject(new Error(`SMTP error: ${line}`));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (step < commands.length) {
|
|
1814
|
+
socket.write(commands[step]);
|
|
1815
|
+
step++;
|
|
1816
|
+
}
|
|
1817
|
+
if (step >= commands.length && code === 221) {
|
|
1818
|
+
socket.end();
|
|
1819
|
+
resolve();
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
socket.on("timeout", () => {
|
|
1825
|
+
socket.destroy();
|
|
1826
|
+
reject(new Error("SMTP connection timed out"));
|
|
1827
|
+
});
|
|
1828
|
+
socket.on("error", (err) => {
|
|
1829
|
+
reject(new Error(`SMTP error: ${err.message}`));
|
|
1830
|
+
});
|
|
1831
|
+
};
|
|
1832
|
+
if (!this.config.secure && this.config.port === 587) {
|
|
1833
|
+
const plain = net2.createConnection({
|
|
1834
|
+
host: this.config.host,
|
|
1835
|
+
port: this.config.port
|
|
1836
|
+
});
|
|
1837
|
+
plain.setTimeout(timeout);
|
|
1838
|
+
let buf = "";
|
|
1839
|
+
let startTlsSent = false;
|
|
1840
|
+
let ehloSent = false;
|
|
1841
|
+
plain.on("data", (data) => {
|
|
1842
|
+
buf += data.toString();
|
|
1843
|
+
const lines = buf.split("\r\n");
|
|
1844
|
+
buf = lines.pop() ?? "";
|
|
1845
|
+
for (const line of lines) {
|
|
1846
|
+
if (!line) continue;
|
|
1847
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
1848
|
+
if (line.charAt(3) === "-") continue;
|
|
1849
|
+
if (code >= 400) {
|
|
1850
|
+
plain.destroy();
|
|
1851
|
+
reject(new Error(`SMTP error: ${line}`));
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
if (!ehloSent) {
|
|
1855
|
+
plain.write("EHLO aegis\r\n");
|
|
1856
|
+
ehloSent = true;
|
|
1857
|
+
} else if (!startTlsSent) {
|
|
1858
|
+
plain.write("STARTTLS\r\n");
|
|
1859
|
+
startTlsSent = true;
|
|
1860
|
+
} else if (code === 220 && startTlsSent) {
|
|
1861
|
+
socket = tls.connect({
|
|
1862
|
+
socket: plain,
|
|
1863
|
+
host: this.config.host,
|
|
1864
|
+
rejectUnauthorized: true
|
|
1865
|
+
}, () => {
|
|
1866
|
+
let step2 = 0;
|
|
1867
|
+
let buffer2 = "";
|
|
1868
|
+
const cmds = [
|
|
1869
|
+
`EHLO aegis\r
|
|
1870
|
+
`,
|
|
1871
|
+
`AUTH LOGIN\r
|
|
1872
|
+
`,
|
|
1873
|
+
`${Buffer.from(this.config.username).toString("base64")}\r
|
|
1874
|
+
`,
|
|
1875
|
+
`${Buffer.from(this.config.password).toString("base64")}\r
|
|
1876
|
+
`,
|
|
1877
|
+
`MAIL FROM:<${this.config.from}>\r
|
|
1878
|
+
`,
|
|
1879
|
+
`RCPT TO:<${this.config.to}>\r
|
|
1880
|
+
`,
|
|
1881
|
+
`DATA\r
|
|
1882
|
+
`,
|
|
1883
|
+
`From: ${this.config.from}\r
|
|
1884
|
+
To: ${this.config.to}\r
|
|
1885
|
+
Subject: ${subject}\r
|
|
1886
|
+
Content-Type: text/plain; charset=utf-8\r
|
|
1887
|
+
X-Mailer: OpenClaw-Aegis\r
|
|
1888
|
+
\r
|
|
1889
|
+
${body}\r
|
|
1890
|
+
.\r
|
|
1891
|
+
`,
|
|
1892
|
+
`QUIT\r
|
|
1893
|
+
`
|
|
1894
|
+
];
|
|
1895
|
+
socket.write(cmds[0]);
|
|
1896
|
+
step2 = 1;
|
|
1897
|
+
socket.on("data", (d) => {
|
|
1898
|
+
buffer2 += d.toString();
|
|
1899
|
+
const ls = buffer2.split("\r\n");
|
|
1900
|
+
buffer2 = ls.pop() ?? "";
|
|
1901
|
+
for (const l of ls) {
|
|
1902
|
+
if (!l) continue;
|
|
1903
|
+
const c = parseInt(l.substring(0, 3), 10);
|
|
1904
|
+
if (l.charAt(3) === "-") continue;
|
|
1905
|
+
if (c >= 400) {
|
|
1906
|
+
socket.destroy();
|
|
1907
|
+
reject(new Error(`SMTP error: ${l}`));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (step2 < cmds.length) {
|
|
1911
|
+
socket.write(cmds[step2]);
|
|
1912
|
+
step2++;
|
|
1913
|
+
}
|
|
1914
|
+
if (step2 >= cmds.length && c === 221) {
|
|
1915
|
+
socket.end();
|
|
1916
|
+
resolve();
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
plain.on("timeout", () => {
|
|
1927
|
+
plain.destroy();
|
|
1928
|
+
reject(new Error("SMTP connection timed out"));
|
|
1929
|
+
});
|
|
1930
|
+
plain.on("error", (err) => {
|
|
1931
|
+
reject(new Error(`SMTP error: ${err.message}`));
|
|
1932
|
+
});
|
|
1933
|
+
} else {
|
|
1934
|
+
connect2();
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
|
|
1940
|
+
// src/alerts/providers/pushover.ts
|
|
1941
|
+
var PushoverProvider = class {
|
|
1942
|
+
name = "pushover";
|
|
1943
|
+
config;
|
|
1944
|
+
constructor(config) {
|
|
1945
|
+
this.config = config;
|
|
1946
|
+
}
|
|
1947
|
+
async send(alert) {
|
|
1948
|
+
const start = Date.now();
|
|
1949
|
+
const priority = alert.severity === "critical" ? 1 : alert.severity === "warning" ? 0 : -1;
|
|
1950
|
+
const params = {
|
|
1951
|
+
token: this.config.apiToken,
|
|
1952
|
+
user: this.config.userKey,
|
|
1953
|
+
title: alert.title,
|
|
1954
|
+
message: alert.body,
|
|
1955
|
+
priority: String(priority),
|
|
1956
|
+
timestamp: String(Math.floor(new Date(alert.timestamp).getTime() / 1e3))
|
|
1957
|
+
};
|
|
1958
|
+
if (this.config.device) {
|
|
1959
|
+
params.device = this.config.device;
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const response = await fetch("https://api.pushover.net/1/messages.json", {
|
|
1963
|
+
method: "POST",
|
|
1964
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1965
|
+
body: new URLSearchParams(params).toString()
|
|
1966
|
+
});
|
|
1967
|
+
const data = await response.json();
|
|
1968
|
+
const ok = data.status === 1;
|
|
1969
|
+
return {
|
|
1970
|
+
provider: this.name,
|
|
1971
|
+
success: ok,
|
|
1972
|
+
error: ok ? void 0 : data.errors?.join(", ") ?? `HTTP ${response.status}`,
|
|
1973
|
+
durationMs: Date.now() - start
|
|
1974
|
+
};
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
return {
|
|
1977
|
+
provider: this.name,
|
|
1978
|
+
success: false,
|
|
1979
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1980
|
+
durationMs: Date.now() - start
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async test() {
|
|
1985
|
+
try {
|
|
1986
|
+
const result = await this.send({
|
|
1987
|
+
severity: "info",
|
|
1988
|
+
title: "Aegis Alert Test",
|
|
1989
|
+
body: "This is a test alert from OpenClaw Aegis.",
|
|
1990
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1991
|
+
});
|
|
1992
|
+
return result.success;
|
|
1993
|
+
} catch {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
// src/cli/commands/test-alert.ts
|
|
2000
|
+
var testAlertCommand = new import_commander4.Command("test-alert").description("Send a test alert to all configured channels").option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_PATH).action(async (opts) => {
|
|
2001
|
+
const config = loadConfig(opts.config);
|
|
2002
|
+
if (config.alerts.channels.length === 0) {
|
|
2003
|
+
console.log("No alert channels configured. Run 'aegis init' to add one.");
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
const dispatcher = new AlertDispatcher(1, [1e3]);
|
|
2007
|
+
for (const ch of config.alerts.channels) {
|
|
2008
|
+
switch (ch.type) {
|
|
2009
|
+
case "ntfy":
|
|
2010
|
+
dispatcher.addProvider(new NtfyProvider(ch));
|
|
2011
|
+
break;
|
|
2012
|
+
case "telegram":
|
|
2013
|
+
dispatcher.addProvider(new TelegramProvider(ch));
|
|
2014
|
+
break;
|
|
2015
|
+
case "whatsapp":
|
|
2016
|
+
dispatcher.addProvider(new WhatsAppProvider(ch));
|
|
2017
|
+
break;
|
|
2018
|
+
case "webhook":
|
|
2019
|
+
dispatcher.addProvider(new WebhookProvider(ch));
|
|
2020
|
+
break;
|
|
2021
|
+
case "slack":
|
|
2022
|
+
dispatcher.addProvider(new SlackProvider(ch));
|
|
2023
|
+
break;
|
|
2024
|
+
case "discord":
|
|
2025
|
+
dispatcher.addProvider(new DiscordProvider(ch));
|
|
2026
|
+
break;
|
|
2027
|
+
case "email":
|
|
2028
|
+
dispatcher.addProvider(new EmailProvider(ch));
|
|
2029
|
+
break;
|
|
2030
|
+
case "pushover":
|
|
2031
|
+
dispatcher.addProvider(new PushoverProvider(ch));
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
const alert = {
|
|
2036
|
+
severity: "info",
|
|
2037
|
+
title: "Aegis Test Alert",
|
|
2038
|
+
body: "This is a test notification from OpenClaw Aegis. If you see this, alerts are working.",
|
|
2039
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2040
|
+
};
|
|
2041
|
+
console.log(`Sending test alert to ${dispatcher.getProviders().length} channel(s)...
|
|
2042
|
+
`);
|
|
2043
|
+
const result = await dispatcher.dispatch(alert);
|
|
2044
|
+
for (const r of result.results) {
|
|
2045
|
+
if (r.success) {
|
|
2046
|
+
console.log(` + ${r.provider}: sent (${r.durationMs}ms)`);
|
|
2047
|
+
} else {
|
|
2048
|
+
console.log(` - ${r.provider}: failed \u2014 ${r.error}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
console.log(result.sent ? "\nTest alert sent successfully." : "\nAll channels failed.");
|
|
2052
|
+
process.exit(result.sent ? 0 : 1);
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
// src/cli/index.ts
|
|
2056
|
+
var program = new import_commander5.Command();
|
|
2057
|
+
program.name("aegis").description("OpenClaw Aegis \u2014 self-healing sidecar for the OpenClaw gateway").version("1.0.0");
|
|
2058
|
+
program.addCommand(initCommand);
|
|
2059
|
+
program.addCommand(statusCommand);
|
|
2060
|
+
program.addCommand(checkCommand);
|
|
2061
|
+
program.addCommand(testAlertCommand);
|
|
2062
|
+
program.parse(process.argv);
|
|
2063
|
+
//# sourceMappingURL=index.js.map
|