@sleep2agi/commhub-server 0.8.1-preview.2 → 0.8.1-preview.4
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/package.json +1 -1
- package/src/db.ts +75 -3
- package/src/index.ts +311 -1
- package/src/tools.ts +76 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.8.1-preview.
|
|
3
|
+
"version": "0.8.1-preview.4",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/db.ts
CHANGED
|
@@ -19,6 +19,18 @@ db.exec(`
|
|
|
19
19
|
output TEXT,
|
|
20
20
|
progress INTEGER DEFAULT 0,
|
|
21
21
|
score REAL,
|
|
22
|
+
cpu_load_1min REAL,
|
|
23
|
+
cpu_cores INTEGER,
|
|
24
|
+
mem_total_gb REAL,
|
|
25
|
+
mem_used_gb REAL,
|
|
26
|
+
mem_avail_gb REAL,
|
|
27
|
+
disk_total_gb REAL,
|
|
28
|
+
disk_used_gb REAL,
|
|
29
|
+
disk_avail_gb REAL,
|
|
30
|
+
process_rss_bytes INTEGER,
|
|
31
|
+
process_cpu_pct REAL,
|
|
32
|
+
process_uptime_seconds REAL,
|
|
33
|
+
process_in_flight_count INTEGER,
|
|
22
34
|
network_id TEXT NOT NULL DEFAULT 'default',
|
|
23
35
|
registered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
36
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
@@ -55,7 +67,7 @@ db.exec(`
|
|
|
55
67
|
|
|
56
68
|
// ── V2 schema migration (ALTER TABLE, safe to re-run) ──
|
|
57
69
|
|
|
58
|
-
// sessions: add node_id, session_id, config_path, channels, last_seen_at, model
|
|
70
|
+
// sessions: add node_id, session_id, config_path, channels, last_seen_at, model, host telemetry
|
|
59
71
|
for (const col of [
|
|
60
72
|
{ name: "node_id", def: "TEXT" },
|
|
61
73
|
{ name: "session_id", def: "TEXT" },
|
|
@@ -63,10 +75,51 @@ for (const col of [
|
|
|
63
75
|
{ name: "channels", def: "TEXT" },
|
|
64
76
|
{ name: "last_seen_at", def: "TEXT" },
|
|
65
77
|
{ name: "model", def: "TEXT" },
|
|
78
|
+
{ name: "cpu_load_1min", def: "REAL" },
|
|
79
|
+
{ name: "cpu_cores", def: "INTEGER" },
|
|
80
|
+
{ name: "mem_total_gb", def: "REAL" },
|
|
81
|
+
{ name: "mem_used_gb", def: "REAL" },
|
|
82
|
+
{ name: "mem_avail_gb", def: "REAL" },
|
|
83
|
+
{ name: "disk_total_gb", def: "REAL" },
|
|
84
|
+
{ name: "disk_used_gb", def: "REAL" },
|
|
85
|
+
{ name: "disk_avail_gb", def: "REAL" },
|
|
86
|
+
{ name: "process_rss_bytes", def: "INTEGER" },
|
|
87
|
+
{ name: "process_cpu_pct", def: "REAL" },
|
|
88
|
+
{ name: "process_uptime_seconds", def: "REAL" },
|
|
89
|
+
{ name: "process_in_flight_count", def: "INTEGER" },
|
|
66
90
|
]) {
|
|
67
91
|
try { db.exec(`ALTER TABLE sessions ADD COLUMN ${col.name} ${col.def}`); } catch {}
|
|
68
92
|
}
|
|
69
93
|
|
|
94
|
+
db.exec(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS agent_telemetry (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
network_id TEXT NOT NULL DEFAULT 'default',
|
|
98
|
+
resume_id TEXT,
|
|
99
|
+
alias TEXT,
|
|
100
|
+
hostname TEXT,
|
|
101
|
+
ip TEXT,
|
|
102
|
+
cpu_load_1min REAL,
|
|
103
|
+
cpu_cores INTEGER,
|
|
104
|
+
mem_total_gb REAL,
|
|
105
|
+
mem_used_gb REAL,
|
|
106
|
+
mem_avail_gb REAL,
|
|
107
|
+
disk_total_gb REAL,
|
|
108
|
+
disk_used_gb REAL,
|
|
109
|
+
disk_avail_gb REAL,
|
|
110
|
+
process_rss_bytes INTEGER,
|
|
111
|
+
process_cpu_pct REAL,
|
|
112
|
+
process_uptime_seconds REAL,
|
|
113
|
+
process_in_flight_count INTEGER,
|
|
114
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_agent_telemetry_host_time
|
|
118
|
+
ON agent_telemetry(network_id, hostname, ip, created_at);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_agent_telemetry_alias_time
|
|
120
|
+
ON agent_telemetry(network_id, alias, created_at);
|
|
121
|
+
`);
|
|
122
|
+
|
|
70
123
|
// inbox: add in_reply_to, requires_response, expires_at, scope
|
|
71
124
|
for (const col of [
|
|
72
125
|
{ name: "in_reply_to", def: "TEXT" },
|
|
@@ -376,6 +429,19 @@ function migrateSessionsNetworkAliasUnique() {
|
|
|
376
429
|
config_path TEXT,
|
|
377
430
|
channels TEXT,
|
|
378
431
|
last_seen_at TEXT,
|
|
432
|
+
model TEXT,
|
|
433
|
+
cpu_load_1min REAL,
|
|
434
|
+
cpu_cores INTEGER,
|
|
435
|
+
mem_total_gb REAL,
|
|
436
|
+
mem_used_gb REAL,
|
|
437
|
+
mem_avail_gb REAL,
|
|
438
|
+
disk_total_gb REAL,
|
|
439
|
+
disk_used_gb REAL,
|
|
440
|
+
disk_avail_gb REAL,
|
|
441
|
+
process_rss_bytes INTEGER,
|
|
442
|
+
process_cpu_pct REAL,
|
|
443
|
+
process_uptime_seconds REAL,
|
|
444
|
+
process_in_flight_count INTEGER,
|
|
379
445
|
network_id TEXT NOT NULL DEFAULT 'default',
|
|
380
446
|
UNIQUE (network_id, alias)
|
|
381
447
|
)
|
|
@@ -384,12 +450,18 @@ function migrateSessionsNetworkAliasUnique() {
|
|
|
384
450
|
INSERT OR REPLACE INTO sessions_migrated (
|
|
385
451
|
resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version,
|
|
386
452
|
status, task, output, progress, score, registered_at, updated_at, node_id,
|
|
387
|
-
session_id, config_path, channels, last_seen_at,
|
|
453
|
+
session_id, config_path, channels, last_seen_at, model, cpu_load_1min,
|
|
454
|
+
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb,
|
|
455
|
+
disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct,
|
|
456
|
+
process_uptime_seconds, process_in_flight_count, network_id
|
|
388
457
|
)
|
|
389
458
|
SELECT
|
|
390
459
|
resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version,
|
|
391
460
|
status, task, output, progress, score, registered_at, updated_at, node_id,
|
|
392
|
-
session_id, config_path, channels, last_seen_at,
|
|
461
|
+
session_id, config_path, channels, last_seen_at, model, cpu_load_1min,
|
|
462
|
+
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb,
|
|
463
|
+
disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct,
|
|
464
|
+
process_uptime_seconds, process_in_flight_count,
|
|
393
465
|
COALESCE(NULLIF(network_id, ''), 'default')
|
|
394
466
|
FROM sessions
|
|
395
467
|
ORDER BY updated_at
|
package/src/index.ts
CHANGED
|
@@ -229,6 +229,104 @@ function singleNetworkId(scope: RestNetworkScope): string | null {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function sqliteTime(date: Date): string {
|
|
233
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseSqliteTime(value: string | null | undefined): number {
|
|
237
|
+
if (!value) return 0;
|
|
238
|
+
const normalized = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
|
|
239
|
+
const ts = Date.parse(normalized);
|
|
240
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cpuPct(load: number | null | undefined, cores: number | null | undefined): number | null {
|
|
244
|
+
if (typeof load !== "number" || typeof cores !== "number" || cores <= 0) return null;
|
|
245
|
+
return Math.round((load / cores) * 1000) / 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function serverAlertLevel(row: any): { level: "green" | "yellow" | "red"; alerts: string[] } {
|
|
249
|
+
const alerts: string[] = [];
|
|
250
|
+
const pct = cpuPct(row?.cpu_load_1min, row?.cpu_cores);
|
|
251
|
+
if (pct !== null && pct >= 80) alerts.push(`cpu ${pct}%`);
|
|
252
|
+
if (typeof row?.mem_avail_gb === "number" && row.mem_avail_gb < 0.5) alerts.push(`memory ${row.mem_avail_gb}GB available`);
|
|
253
|
+
if (typeof row?.disk_avail_gb === "number" && row.disk_avail_gb < 1) alerts.push(`disk ${row.disk_avail_gb}GB available`);
|
|
254
|
+
if (alerts.length > 0) return { level: "red", alerts };
|
|
255
|
+
|
|
256
|
+
if (pct !== null && pct >= 60) alerts.push(`cpu ${pct}%`);
|
|
257
|
+
if (typeof row?.mem_avail_gb === "number" && row.mem_avail_gb < 1) alerts.push(`memory ${row.mem_avail_gb}GB available`);
|
|
258
|
+
if (typeof row?.disk_avail_gb === "number" && row.disk_avail_gb < 5) alerts.push(`disk ${row.disk_avail_gb}GB available`);
|
|
259
|
+
return { level: alerts.length > 0 ? "yellow" : "green", alerts };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function agentHealthChip(status: unknown, lastSeen: string | null | undefined): "online" | "offline" | "stale" {
|
|
263
|
+
if (String(status || "").toLowerCase() === "offline") return "offline";
|
|
264
|
+
const ts = parseSqliteTime(lastSeen);
|
|
265
|
+
if (!ts || Date.now() - ts > 5 * 60 * 1000) return "stale";
|
|
266
|
+
return "online";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function bucketTelemetry(rows: any[], fromMs: number, bucketMs: number) {
|
|
270
|
+
const buckets = new Map<number, {
|
|
271
|
+
ts: number;
|
|
272
|
+
count: number;
|
|
273
|
+
cpu_pct_sum: number;
|
|
274
|
+
cpu_pct_count: number;
|
|
275
|
+
cpu_load_sum: number;
|
|
276
|
+
cpu_load_count: number;
|
|
277
|
+
mem_avail_min: number | null;
|
|
278
|
+
mem_used_max: number | null;
|
|
279
|
+
disk_avail_min: number | null;
|
|
280
|
+
disk_used_max: number | null;
|
|
281
|
+
}>();
|
|
282
|
+
|
|
283
|
+
for (const row of rows) {
|
|
284
|
+
const ts = parseSqliteTime(row.created_at);
|
|
285
|
+
if (!ts || ts < fromMs) continue;
|
|
286
|
+
const bucketTs = Math.floor(ts / bucketMs) * bucketMs;
|
|
287
|
+
const bucket = buckets.get(bucketTs) ?? {
|
|
288
|
+
ts: bucketTs,
|
|
289
|
+
count: 0,
|
|
290
|
+
cpu_pct_sum: 0,
|
|
291
|
+
cpu_pct_count: 0,
|
|
292
|
+
cpu_load_sum: 0,
|
|
293
|
+
cpu_load_count: 0,
|
|
294
|
+
mem_avail_min: null,
|
|
295
|
+
mem_used_max: null,
|
|
296
|
+
disk_avail_min: null,
|
|
297
|
+
disk_used_max: null,
|
|
298
|
+
};
|
|
299
|
+
bucket.count += 1;
|
|
300
|
+
const pct = cpuPct(row.cpu_load_1min, row.cpu_cores);
|
|
301
|
+
if (pct !== null) {
|
|
302
|
+
bucket.cpu_pct_sum += pct;
|
|
303
|
+
bucket.cpu_pct_count += 1;
|
|
304
|
+
}
|
|
305
|
+
if (typeof row.cpu_load_1min === "number") {
|
|
306
|
+
bucket.cpu_load_sum += row.cpu_load_1min;
|
|
307
|
+
bucket.cpu_load_count += 1;
|
|
308
|
+
}
|
|
309
|
+
if (typeof row.mem_avail_gb === "number") bucket.mem_avail_min = bucket.mem_avail_min === null ? row.mem_avail_gb : Math.min(bucket.mem_avail_min, row.mem_avail_gb);
|
|
310
|
+
if (typeof row.mem_used_gb === "number") bucket.mem_used_max = bucket.mem_used_max === null ? row.mem_used_gb : Math.max(bucket.mem_used_max, row.mem_used_gb);
|
|
311
|
+
if (typeof row.disk_avail_gb === "number") bucket.disk_avail_min = bucket.disk_avail_min === null ? row.disk_avail_gb : Math.min(bucket.disk_avail_min, row.disk_avail_gb);
|
|
312
|
+
if (typeof row.disk_used_gb === "number") bucket.disk_used_max = bucket.disk_used_max === null ? row.disk_used_gb : Math.max(bucket.disk_used_max, row.disk_used_gb);
|
|
313
|
+
buckets.set(bucketTs, bucket);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return Array.from(buckets.values())
|
|
317
|
+
.sort((a, b) => a.ts - b.ts)
|
|
318
|
+
.map((b) => ({
|
|
319
|
+
ts: new Date(b.ts).toISOString(),
|
|
320
|
+
count: b.count,
|
|
321
|
+
cpu_pct: b.cpu_pct_count ? Math.round((b.cpu_pct_sum / b.cpu_pct_count) * 10) / 10 : null,
|
|
322
|
+
cpu_load_1min: b.cpu_load_count ? Math.round((b.cpu_load_sum / b.cpu_load_count) * 100) / 100 : null,
|
|
323
|
+
mem_avail_gb: b.mem_avail_min,
|
|
324
|
+
mem_used_gb: b.mem_used_max,
|
|
325
|
+
disk_avail_gb: b.disk_avail_min,
|
|
326
|
+
disk_used_gb: b.disk_used_max,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
232
330
|
function canRestWriteNetwork(authCtx: { userId: string; networkId: string | null } | null, networkId: string | null, isAdmin: boolean): boolean {
|
|
233
331
|
if (!authCtx) return true; // legacy global token or open dev mode
|
|
234
332
|
if (isAdmin) return true;
|
|
@@ -266,7 +364,7 @@ function corsHeaders(req: Request): Record<string, string> {
|
|
|
266
364
|
const allowed = CORS_ORIGINS.includes(origin) ? origin : "";
|
|
267
365
|
return {
|
|
268
366
|
"Access-Control-Allow-Origin": allowed,
|
|
269
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
367
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
270
368
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
271
369
|
"Access-Control-Max-Age": "86400",
|
|
272
370
|
};
|
|
@@ -841,6 +939,172 @@ Bun.serve({
|
|
|
841
939
|
return withCors(req, Response.json({ ok: true, sessions, summary }));
|
|
842
940
|
}
|
|
843
941
|
|
|
942
|
+
// ── REST: aggregate agents by physical server ──
|
|
943
|
+
if (url.pathname === "/api/servers") {
|
|
944
|
+
const cutoff = new Date(Date.now() - 10 * 60 * 1000).toISOString().replace("T", " ").slice(0, 19);
|
|
945
|
+
const staleParams: any[] = [cutoff];
|
|
946
|
+
let staleSql = "UPDATE sessions SET status = 'offline' WHERE updated_at < ?1 AND status != 'offline'";
|
|
947
|
+
staleSql = addNetworkScope(staleSql, staleParams, restScope);
|
|
948
|
+
db.run(staleSql, staleParams);
|
|
949
|
+
|
|
950
|
+
const params: any[] = [];
|
|
951
|
+
let sql = `
|
|
952
|
+
SELECT hostname, ip, cpu_load_1min, cpu_cores, mem_avail_gb, mem_used_gb,
|
|
953
|
+
COALESCE(last_seen_at, updated_at) AS last_seen
|
|
954
|
+
FROM sessions
|
|
955
|
+
WHERE 1=1
|
|
956
|
+
`;
|
|
957
|
+
sql = addNetworkScope(sql, params, restScope);
|
|
958
|
+
sql += " ORDER BY COALESCE(last_seen_at, updated_at) DESC";
|
|
959
|
+
|
|
960
|
+
const grouped = new Map<string, {
|
|
961
|
+
hostname: string;
|
|
962
|
+
ip: string;
|
|
963
|
+
agent_count: number;
|
|
964
|
+
cpu_load_1min: number | null;
|
|
965
|
+
cpu_cores: number | null;
|
|
966
|
+
mem_avail_gb: number | null;
|
|
967
|
+
mem_used_gb: number | null;
|
|
968
|
+
last_seen: string | null;
|
|
969
|
+
}>();
|
|
970
|
+
|
|
971
|
+
for (const row of db.all<any>(sql, ...params)) {
|
|
972
|
+
const hostname = row.hostname || "unknown";
|
|
973
|
+
const ip = row.ip || "unknown";
|
|
974
|
+
const key = `${hostname}\u0000${ip}`;
|
|
975
|
+
const existing = grouped.get(key);
|
|
976
|
+
if (existing) {
|
|
977
|
+
existing.agent_count += 1;
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
grouped.set(key, {
|
|
981
|
+
hostname,
|
|
982
|
+
ip,
|
|
983
|
+
agent_count: 1,
|
|
984
|
+
cpu_load_1min: row.cpu_load_1min ?? null,
|
|
985
|
+
cpu_cores: row.cpu_cores ?? null,
|
|
986
|
+
mem_avail_gb: row.mem_avail_gb ?? null,
|
|
987
|
+
mem_used_gb: row.mem_used_gb ?? null,
|
|
988
|
+
last_seen: row.last_seen ?? null,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return withCors(req, Response.json(Array.from(grouped.values())));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const serverDetailMatch = url.pathname.match(/^\/api\/server\/([^/]+)\/(health|agents)$/);
|
|
996
|
+
if (serverDetailMatch && req.method === "GET") {
|
|
997
|
+
const host = decodeURIComponent(serverDetailMatch[1]);
|
|
998
|
+
const detailKind = serverDetailMatch[2];
|
|
999
|
+
if (!host) return withCors(req, Response.json({ ok: false, error: "host required" }, { status: 400 }));
|
|
1000
|
+
|
|
1001
|
+
const cutoff = sqliteTime(new Date(Date.now() - 10 * 60 * 1000));
|
|
1002
|
+
const staleParams: any[] = [cutoff];
|
|
1003
|
+
let staleSql = "UPDATE sessions SET status = 'offline' WHERE updated_at < ?1 AND status != 'offline'";
|
|
1004
|
+
staleSql = addNetworkScope(staleSql, staleParams, restScope);
|
|
1005
|
+
db.run(staleSql, staleParams);
|
|
1006
|
+
|
|
1007
|
+
if (detailKind === "agents") {
|
|
1008
|
+
const params: any[] = [host, host];
|
|
1009
|
+
let sql = `
|
|
1010
|
+
SELECT alias, agent, status, task, progress, model, hostname, ip,
|
|
1011
|
+
cpu_load_1min, cpu_cores, mem_avail_gb, mem_used_gb, mem_total_gb,
|
|
1012
|
+
disk_avail_gb, disk_used_gb, disk_total_gb,
|
|
1013
|
+
process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count,
|
|
1014
|
+
COALESCE(last_seen_at, updated_at) AS last_seen
|
|
1015
|
+
FROM sessions
|
|
1016
|
+
WHERE (hostname = ?1 OR ip = ?2)
|
|
1017
|
+
`;
|
|
1018
|
+
sql = addNetworkScope(sql, params, restScope);
|
|
1019
|
+
sql += " ORDER BY alias";
|
|
1020
|
+
const agents = db.all<any>(sql, ...params).map((s) => ({
|
|
1021
|
+
alias: s.alias,
|
|
1022
|
+
runtime: normalizeRuntime(s.agent),
|
|
1023
|
+
raw_agent: s.agent ?? null,
|
|
1024
|
+
model: s.model ?? null,
|
|
1025
|
+
status: s.status ?? "offline",
|
|
1026
|
+
task: s.task ?? null,
|
|
1027
|
+
progress: s.progress ?? 0,
|
|
1028
|
+
last_seen: s.last_seen ?? null,
|
|
1029
|
+
health: agentHealthChip(s.status, s.last_seen),
|
|
1030
|
+
hostname: s.hostname ?? null,
|
|
1031
|
+
ip: s.ip ?? null,
|
|
1032
|
+
telemetry: {
|
|
1033
|
+
cpu_load_1min: s.cpu_load_1min ?? null,
|
|
1034
|
+
cpu_cores: s.cpu_cores ?? null,
|
|
1035
|
+
cpu_pct: cpuPct(s.cpu_load_1min, s.cpu_cores),
|
|
1036
|
+
mem_total_gb: s.mem_total_gb ?? null,
|
|
1037
|
+
mem_used_gb: s.mem_used_gb ?? null,
|
|
1038
|
+
mem_avail_gb: s.mem_avail_gb ?? null,
|
|
1039
|
+
disk_total_gb: s.disk_total_gb ?? null,
|
|
1040
|
+
disk_used_gb: s.disk_used_gb ?? null,
|
|
1041
|
+
disk_avail_gb: s.disk_avail_gb ?? null,
|
|
1042
|
+
process_rss_bytes: s.process_rss_bytes ?? null,
|
|
1043
|
+
process_cpu_pct: s.process_cpu_pct ?? null,
|
|
1044
|
+
process_uptime_seconds: s.process_uptime_seconds ?? null,
|
|
1045
|
+
process_in_flight_count: s.process_in_flight_count ?? null,
|
|
1046
|
+
},
|
|
1047
|
+
}));
|
|
1048
|
+
if (agents.length === 0) return withCors(req, Response.json({ ok: false, error: "server not found" }, { status: 404 }));
|
|
1049
|
+
return withCors(req, Response.json({ ok: true, host, agent_count: agents.length, agents }));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const latestParams: any[] = [host, host];
|
|
1053
|
+
let latestSql = `
|
|
1054
|
+
SELECT hostname, ip, COUNT(*) OVER () AS agent_count,
|
|
1055
|
+
cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
1056
|
+
disk_total_gb, disk_used_gb, disk_avail_gb,
|
|
1057
|
+
COALESCE(last_seen_at, updated_at) AS last_seen
|
|
1058
|
+
FROM sessions
|
|
1059
|
+
WHERE (hostname = ?1 OR ip = ?2)
|
|
1060
|
+
`;
|
|
1061
|
+
latestSql = addNetworkScope(latestSql, latestParams, restScope);
|
|
1062
|
+
latestSql += " ORDER BY COALESCE(last_seen_at, updated_at) DESC LIMIT 1";
|
|
1063
|
+
const latest = db.get<any>(latestSql, ...latestParams);
|
|
1064
|
+
if (!latest) return withCors(req, Response.json({ ok: false, error: "server not found" }, { status: 404 }));
|
|
1065
|
+
|
|
1066
|
+
const since24h = sqliteTime(new Date(Date.now() - 24 * 60 * 60 * 1000));
|
|
1067
|
+
const histParams: any[] = [host, host, since24h];
|
|
1068
|
+
let histSql = `
|
|
1069
|
+
SELECT created_at, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
1070
|
+
disk_total_gb, disk_used_gb, disk_avail_gb
|
|
1071
|
+
FROM agent_telemetry
|
|
1072
|
+
WHERE (hostname = ?1 OR ip = ?2) AND created_at >= ?3
|
|
1073
|
+
`;
|
|
1074
|
+
histSql = addNetworkScope(histSql, histParams, restScope);
|
|
1075
|
+
histSql += " ORDER BY created_at ASC";
|
|
1076
|
+
const historyRows = db.all<any>(histSql, ...histParams);
|
|
1077
|
+
const now = Date.now();
|
|
1078
|
+
const alert = serverAlertLevel(latest);
|
|
1079
|
+
|
|
1080
|
+
return withCors(req, Response.json({
|
|
1081
|
+
ok: true,
|
|
1082
|
+
host,
|
|
1083
|
+
hostname: latest.hostname ?? null,
|
|
1084
|
+
ip: latest.ip ?? null,
|
|
1085
|
+
agent_count: latest.agent_count ?? 0,
|
|
1086
|
+
alert_level: alert.level,
|
|
1087
|
+
alerts: alert.alerts,
|
|
1088
|
+
latest: {
|
|
1089
|
+
cpu_load_1min: latest.cpu_load_1min ?? null,
|
|
1090
|
+
cpu_cores: latest.cpu_cores ?? null,
|
|
1091
|
+
cpu_pct: cpuPct(latest.cpu_load_1min, latest.cpu_cores),
|
|
1092
|
+
mem_total_gb: latest.mem_total_gb ?? null,
|
|
1093
|
+
mem_used_gb: latest.mem_used_gb ?? null,
|
|
1094
|
+
mem_avail_gb: latest.mem_avail_gb ?? null,
|
|
1095
|
+
disk_total_gb: latest.disk_total_gb ?? null,
|
|
1096
|
+
disk_used_gb: latest.disk_used_gb ?? null,
|
|
1097
|
+
disk_avail_gb: latest.disk_avail_gb ?? null,
|
|
1098
|
+
last_seen: latest.last_seen ?? null,
|
|
1099
|
+
},
|
|
1100
|
+
history: {
|
|
1101
|
+
"5m": bucketTelemetry(historyRows, now - 5 * 60 * 1000, 60 * 1000),
|
|
1102
|
+
"1h": bucketTelemetry(historyRows, now - 60 * 60 * 1000, 5 * 60 * 1000),
|
|
1103
|
+
"24h": bucketTelemetry(historyRows, now - 24 * 60 * 60 * 1000, 60 * 60 * 1000),
|
|
1104
|
+
},
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
844
1108
|
// ── REST: send task ──
|
|
845
1109
|
if (url.pathname === "/api/task" && req.method === "POST") {
|
|
846
1110
|
let raw: unknown;
|
|
@@ -1109,6 +1373,52 @@ Bun.serve({
|
|
|
1109
1373
|
return withCors(req, Response.json({ ok: true, events: rows, count: rows.length }));
|
|
1110
1374
|
}
|
|
1111
1375
|
|
|
1376
|
+
// ── REST: delete node (Dashboard/CLI remote cleanup) ──
|
|
1377
|
+
const nodeDeleteMatch = url.pathname.match(/^\/api\/nodes\/([^/]+)$/);
|
|
1378
|
+
if (nodeDeleteMatch && req.method === "DELETE") {
|
|
1379
|
+
const ref = decodeURIComponent(nodeDeleteMatch[1]);
|
|
1380
|
+
const params: any[] = [ref, ref, ref];
|
|
1381
|
+
let sql = "SELECT * FROM nodes WHERE (node_id = ?1 OR node_name = ?2 OR alias = ?3)";
|
|
1382
|
+
sql = addNetworkScope(sql, params, restScope);
|
|
1383
|
+
sql += " ORDER BY updated_at DESC LIMIT 1";
|
|
1384
|
+
const node = db.get<any>(sql, ...params);
|
|
1385
|
+
if (!node) return withCors(req, Response.json({ ok: false, error: "node not found" }, { status: 404 }));
|
|
1386
|
+
|
|
1387
|
+
const nodeNetId = node.network_id ?? singleNetworkId(restScope);
|
|
1388
|
+
if (!canRestWriteNetwork(restAuth, nodeNetId, isAdmin)) {
|
|
1389
|
+
return withCors(req, Response.json({ ok: false, error: "permission_denied" }, { status: 403 }));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
db.transaction(() => {
|
|
1393
|
+
db.run("DELETE FROM nodes WHERE node_id = ?1", [node.node_id]);
|
|
1394
|
+
if (node.alias) {
|
|
1395
|
+
db.run(
|
|
1396
|
+
"DELETE FROM sessions WHERE alias = ?1 AND (network_id = ?2 OR (?2 IS NULL AND network_id IS NULL))",
|
|
1397
|
+
[node.alias, node.network_id ?? null]
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
if (node.alias) {
|
|
1403
|
+
pushEvent(node.alias, {
|
|
1404
|
+
type: "node_deleted",
|
|
1405
|
+
node_id: node.node_id,
|
|
1406
|
+
node_name: node.node_name,
|
|
1407
|
+
alias: node.alias,
|
|
1408
|
+
network_id: node.network_id ?? null,
|
|
1409
|
+
}, node.network_id ?? null);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return withCors(req, Response.json({
|
|
1413
|
+
ok: true,
|
|
1414
|
+
deleted: true,
|
|
1415
|
+
node_id: node.node_id,
|
|
1416
|
+
node_name: node.node_name,
|
|
1417
|
+
alias: node.alias,
|
|
1418
|
+
network_id: node.network_id ?? null,
|
|
1419
|
+
}));
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1112
1422
|
// ── REST: nodes table (V2 Sprint 2) ──
|
|
1113
1423
|
if (url.pathname === "/api/nodes") {
|
|
1114
1424
|
const nodeId = url.searchParams.get("node_id");
|
package/src/tools.ts
CHANGED
|
@@ -29,6 +29,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
29
29
|
return !!role && role !== "viewer"; // owner/admin/member can write
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
const writeDeniedReply = (effectiveNetworkId?: string | null, action = "write") => {
|
|
33
|
+
const netId = enforceNetworkId ?? effectiveNetworkId ?? null;
|
|
34
|
+
const message = !netId
|
|
35
|
+
? "network_id required (utok current_network is null; pass explicit network_id from /api/auth/me networks[0].network_id)"
|
|
36
|
+
: action === "send_task"
|
|
37
|
+
? "Viewer role cannot send tasks"
|
|
38
|
+
: "Viewer role cannot write to this network";
|
|
39
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied", message }) }] };
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
const addScope = (sql: string, params: any[], networkId?: string | null, column = "network_id"): string => {
|
|
33
43
|
if (!networkId) return sql;
|
|
34
44
|
sql += ` AND ${column} = ?${params.length + 1}`;
|
|
@@ -109,25 +119,57 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
109
119
|
model: z.string().max(200).optional().describe("AI model name"),
|
|
110
120
|
node_name: z.string().max(200).optional().describe("Stable node display name (may differ from alias)"),
|
|
111
121
|
network_id: z.string().max(200).optional().describe("Network this agent belongs to"),
|
|
122
|
+
host: z.object({
|
|
123
|
+
hostname: z.string().max(200).optional(),
|
|
124
|
+
ip: z.string().max(200).optional(),
|
|
125
|
+
cpu_load_1min: z.number().nullable().optional(),
|
|
126
|
+
cpu_cores: z.number().nullable().optional(),
|
|
127
|
+
mem_total_gb: z.number().nullable().optional(),
|
|
128
|
+
mem_used_gb: z.number().nullable().optional(),
|
|
129
|
+
mem_avail_gb: z.number().nullable().optional(),
|
|
130
|
+
disk_total_gb: z.number().nullable().optional(),
|
|
131
|
+
disk_used_gb: z.number().nullable().optional(),
|
|
132
|
+
disk_avail_gb: z.number().nullable().optional(),
|
|
133
|
+
}).optional().describe("Host telemetry reported by agent-node"),
|
|
134
|
+
process_telemetry: z.object({
|
|
135
|
+
rss: z.number().nullable().optional(),
|
|
136
|
+
cpu_pct: z.number().nullable().optional(),
|
|
137
|
+
uptime_seconds: z.number().nullable().optional(),
|
|
138
|
+
in_flight_count: z.number().nullable().optional(),
|
|
139
|
+
}).optional().describe("Per-agent process telemetry reported by agent-node"),
|
|
112
140
|
},
|
|
113
|
-
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId }) => {
|
|
141
|
+
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId, host, process_telemetry: proc }) => {
|
|
114
142
|
const effectiveNetId = getNetworkId(netId);
|
|
115
143
|
const sessionNetId = effectiveNetId ?? "default";
|
|
116
144
|
if (!callerTokenIsNetwork || !enforceNetworkId) {
|
|
117
145
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "network_token_required" }) }] };
|
|
118
146
|
}
|
|
119
147
|
if (!canWrite(effectiveNetId)) {
|
|
120
|
-
return
|
|
148
|
+
return writeDeniedReply(effectiveNetId);
|
|
121
149
|
}
|
|
122
150
|
console.log(`[${ts()}] ${alias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}`);
|
|
123
151
|
const trimmedOutput = output?.slice(0, 4000);
|
|
152
|
+
const hostHostname = host?.hostname || hn || null;
|
|
153
|
+
const hostIp = host?.ip || clientIP || null;
|
|
154
|
+
const cpuLoad1m = typeof host?.cpu_load_1min === "number" ? host.cpu_load_1min : null;
|
|
155
|
+
const cpuCores = typeof host?.cpu_cores === "number" ? host.cpu_cores : null;
|
|
156
|
+
const memTotalGb = typeof host?.mem_total_gb === "number" ? host.mem_total_gb : null;
|
|
157
|
+
const memUsedGb = typeof host?.mem_used_gb === "number" ? host.mem_used_gb : null;
|
|
158
|
+
const memAvailGb = typeof host?.mem_avail_gb === "number" ? host.mem_avail_gb : null;
|
|
159
|
+
const diskTotalGb = typeof host?.disk_total_gb === "number" ? host.disk_total_gb : null;
|
|
160
|
+
const diskUsedGb = typeof host?.disk_used_gb === "number" ? host.disk_used_gb : null;
|
|
161
|
+
const diskAvailGb = typeof host?.disk_avail_gb === "number" ? host.disk_avail_gb : null;
|
|
162
|
+
const processRssBytes = typeof proc?.rss === "number" ? proc.rss : null;
|
|
163
|
+
const processCpuPct = typeof proc?.cpu_pct === "number" ? proc.cpu_pct : null;
|
|
164
|
+
const processUptimeSeconds = typeof proc?.uptime_seconds === "number" ? proc.uptime_seconds : null;
|
|
165
|
+
const processInFlightCount = typeof proc?.in_flight_count === "number" ? proc.in_flight_count : null;
|
|
124
166
|
|
|
125
167
|
db.transaction(() => {
|
|
126
168
|
// Only delete same-alias sessions within the same network
|
|
127
169
|
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [alias, resume_id, sessionNetId]);
|
|
128
170
|
db.run(
|
|
129
|
-
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, model, last_seen_at, updated_at)
|
|
130
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, datetime('now'), datetime('now'))
|
|
171
|
+
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count, last_seen_at, updated_at)
|
|
172
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, datetime('now'), datetime('now'))
|
|
131
173
|
ON CONFLICT(resume_id) DO UPDATE SET
|
|
132
174
|
alias = COALESCE(?2, sessions.alias), tmux_name = COALESCE(?3, sessions.tmux_name),
|
|
133
175
|
server = COALESCE(?4, sessions.server), ip = COALESCE(?5, sessions.ip),
|
|
@@ -139,9 +181,28 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
139
181
|
session_id = COALESCE(?16, sessions.session_id), config_path = COALESCE(?17, sessions.config_path),
|
|
140
182
|
channels = COALESCE(?18, sessions.channels), network_id = COALESCE(?19, sessions.network_id),
|
|
141
183
|
model = COALESCE(?20, sessions.model),
|
|
184
|
+
cpu_load_1min = COALESCE(?21, sessions.cpu_load_1min),
|
|
185
|
+
cpu_cores = COALESCE(?22, sessions.cpu_cores),
|
|
186
|
+
mem_total_gb = COALESCE(?23, sessions.mem_total_gb),
|
|
187
|
+
mem_used_gb = COALESCE(?24, sessions.mem_used_gb),
|
|
188
|
+
mem_avail_gb = COALESCE(?25, sessions.mem_avail_gb),
|
|
189
|
+
disk_total_gb = COALESCE(?26, sessions.disk_total_gb),
|
|
190
|
+
disk_used_gb = COALESCE(?27, sessions.disk_used_gb),
|
|
191
|
+
disk_avail_gb = COALESCE(?28, sessions.disk_avail_gb),
|
|
192
|
+
process_rss_bytes = COALESCE(?29, sessions.process_rss_bytes),
|
|
193
|
+
process_cpu_pct = COALESCE(?30, sessions.process_cpu_pct),
|
|
194
|
+
process_uptime_seconds = COALESCE(?31, sessions.process_uptime_seconds),
|
|
195
|
+
process_in_flight_count = COALESCE(?32, sessions.process_in_flight_count),
|
|
142
196
|
last_seen_at = datetime('now'), updated_at = datetime('now')`,
|
|
143
|
-
[resume_id, alias, tmux ?? null, srv ?? null,
|
|
197
|
+
[resume_id, alias, tmux ?? null, srv ?? null, hostIp, hostHostname, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, sessionNetId, mdl ?? null, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
144
198
|
);
|
|
199
|
+
if (host || proc) {
|
|
200
|
+
db.run(
|
|
201
|
+
`INSERT INTO agent_telemetry (id, network_id, resume_id, alias, hostname, ip, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count, created_at)
|
|
202
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, datetime('now'))`,
|
|
203
|
+
[uuidv4(), sessionNetId, resume_id, alias, hostHostname, hostIp, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
204
|
+
);
|
|
205
|
+
}
|
|
145
206
|
});
|
|
146
207
|
|
|
147
208
|
// V2: sync tasks table — report_status(working) → tasks.running
|
|
@@ -225,7 +286,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
225
286
|
async ({ alias, task, result, artifacts, score, duration_minutes, network_id: netId }) => {
|
|
226
287
|
const effectiveNetId = getNetworkId(netId);
|
|
227
288
|
if (!canWrite(effectiveNetId)) {
|
|
228
|
-
return
|
|
289
|
+
return writeDeniedReply(effectiveNetId);
|
|
229
290
|
}
|
|
230
291
|
console.log(`[${ts()}] ${alias} → report_completion: ${task.slice(0, 60)}${effectiveNetId ? " [net]" : ""}`);
|
|
231
292
|
const id = uuidv4();
|
|
@@ -337,7 +398,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
337
398
|
},
|
|
338
399
|
async ({ alias, message_id, response }) => {
|
|
339
400
|
const effectiveNetId = getNetworkId(null);
|
|
340
|
-
if (!canWrite(effectiveNetId)) return
|
|
401
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
341
402
|
console.log(`[${ts()}] ${alias} → ack_inbox: ${message_id.slice(0, 8)}`);
|
|
342
403
|
const ackParams: any[] = [message_id, alias];
|
|
343
404
|
let ackSql = "UPDATE inbox SET acked = 1 WHERE id = ?1 AND session_name = ?2";
|
|
@@ -478,7 +539,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
478
539
|
|
|
479
540
|
// Role check: viewer cannot send tasks
|
|
480
541
|
if (!canWrite(effectiveNetId)) {
|
|
481
|
-
return
|
|
542
|
+
return writeDeniedReply(effectiveNetId, "send_task");
|
|
482
543
|
}
|
|
483
544
|
|
|
484
545
|
// License check
|
|
@@ -557,7 +618,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
557
618
|
},
|
|
558
619
|
async ({ alias, message, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
559
620
|
const effectiveNetId = getNetworkId(null);
|
|
560
|
-
if (!canWrite(effectiveNetId)) return
|
|
621
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
561
622
|
console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
|
|
562
623
|
const id = uuidv4();
|
|
563
624
|
db.run(
|
|
@@ -598,7 +659,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
598
659
|
},
|
|
599
660
|
async ({ alias, text, in_reply_to, status: replyStatus, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
600
661
|
const effectiveNetId = getNetworkId(null);
|
|
601
|
-
if (!canWrite(effectiveNetId)) return
|
|
662
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
602
663
|
console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
|
|
603
664
|
const id = uuidv4();
|
|
604
665
|
const replyLogged = db.transaction(() => {
|
|
@@ -673,7 +734,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
673
734
|
},
|
|
674
735
|
async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
675
736
|
const effectiveNetId = getNetworkId(null);
|
|
676
|
-
if (!canWrite(effectiveNetId)) return
|
|
737
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
677
738
|
console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
|
|
678
739
|
const updateParams: any[] = [task_id];
|
|
679
740
|
let updateSql = "UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status IN ('created', 'delivered')";
|
|
@@ -699,7 +760,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
699
760
|
},
|
|
700
761
|
async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
701
762
|
const effectiveNetId = getNetworkId(null);
|
|
702
|
-
if (!canWrite(effectiveNetId)) return
|
|
763
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
703
764
|
console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
|
|
704
765
|
// Find the original task
|
|
705
766
|
const taskParams: any[] = [task_id];
|
|
@@ -810,7 +871,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
810
871
|
},
|
|
811
872
|
async ({ task_id, reason, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
812
873
|
const effectiveNetId = getNetworkId(null);
|
|
813
|
-
if (!canWrite(effectiveNetId)) return
|
|
874
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
814
875
|
console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
|
|
815
876
|
const updateParams: any[] = [reason || "cancelled by " + from_session, task_id];
|
|
816
877
|
let updateSql = `UPDATE tasks SET status = 'cancelled', result = ?1, completed_at = datetime('now')
|
|
@@ -842,7 +903,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
842
903
|
},
|
|
843
904
|
async ({ task_id, new_alias, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
|
|
844
905
|
const effectiveNetId = getNetworkId(null);
|
|
845
|
-
if (!canWrite(effectiveNetId)) return
|
|
906
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
846
907
|
console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
|
|
847
908
|
const taskParams: any[] = [task_id];
|
|
848
909
|
let taskSql = "SELECT * FROM tasks WHERE task_id = ?1";
|
|
@@ -886,7 +947,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
886
947
|
},
|
|
887
948
|
async ({ message, filter_server, filter_status, network_id: netId }) => {
|
|
888
949
|
const effectiveNetId = getNetworkId(netId);
|
|
889
|
-
if (!canWrite(effectiveNetId)) return
|
|
950
|
+
if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
|
|
890
951
|
console.log(`[${ts()}] hub → broadcast: ${message.slice(0, 60)}${effectiveNetId ? " [net=" + effectiveNetId.slice(0, 12) + "]" : ""}`);
|
|
891
952
|
let sql = "SELECT alias, network_id FROM sessions WHERE alias IS NOT NULL";
|
|
892
953
|
const params: any[] = [];
|