@sleep2agi/commhub-server 0.5.0-preview.33 → 0.5.0-preview.35
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/index.ts +15 -12
- package/src/tools.ts +19 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.5.0-preview.
|
|
3
|
+
"version": "0.5.0-preview.35",
|
|
4
4
|
"description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 18 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,14 @@ const PORT = Number(process.env.PORT) || 9200;
|
|
|
10
10
|
const HOST = process.env.HOST || "0.0.0.0";
|
|
11
11
|
const AUTH_TOKEN = process.env.COMMHUB_AUTH_TOKEN;
|
|
12
12
|
|
|
13
|
+
// Read version from package.json so banners and /health stay in sync.
|
|
14
|
+
const SERVER_VERSION = (() => {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL("../package.json", import.meta.url);
|
|
17
|
+
return JSON.parse(require("fs").readFileSync(url, "utf8")).version || "?";
|
|
18
|
+
} catch { return "?"; }
|
|
19
|
+
})();
|
|
20
|
+
|
|
13
21
|
// ── Rate limiter (in-memory, per IP) ──
|
|
14
22
|
const rateLimits = new Map<string, { count: number; resetAt: number }>();
|
|
15
23
|
function checkRateLimit(ip: string, maxPerMinute = 60): boolean {
|
|
@@ -228,17 +236,12 @@ Bun.serve({
|
|
|
228
236
|
if (authErr) return withCors(req, authErr);
|
|
229
237
|
const fwd = req.headers.get("x-forwarded-for");
|
|
230
238
|
const clientIP = fwd ? fwd.split(",")[0].trim() : (req.headers.get("x-real-ip") ?? "unknown");
|
|
231
|
-
// V3: resolve token → enforce network_id in all MCP tools
|
|
239
|
+
// V3: resolve token → enforce network_id in all MCP tools.
|
|
240
|
+
// utok_ (user token, not network-bound) is allowed — the tool layer
|
|
241
|
+
// scopes to the user's accessible networks. Without this Dashboard
|
|
242
|
+
// (which logs in as a user) cannot call send_task.
|
|
232
243
|
const authCtx = resolveRequestAuth(req);
|
|
233
244
|
const enforceNetId = authCtx?.networkId || null;
|
|
234
|
-
// utok_ (no network binding) cannot use MCP — only ntok_/atok_/global token
|
|
235
|
-
if (authCtx && !authCtx.networkId) {
|
|
236
|
-
return withCors(req, Response.json({
|
|
237
|
-
jsonrpc: "2.0",
|
|
238
|
-
error: { code: -32000, message: "User token (utok_) cannot access MCP. Use a network token (ntok_) instead." },
|
|
239
|
-
id: null,
|
|
240
|
-
}, { status: 403 }));
|
|
241
|
-
}
|
|
242
245
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
243
246
|
sessionIdGenerator: undefined,
|
|
244
247
|
});
|
|
@@ -599,7 +602,7 @@ Bun.serve({
|
|
|
599
602
|
const license = db.get<any>("SELECT type, expires_at FROM licenses LIMIT 1");
|
|
600
603
|
return withCors(req, Response.json({
|
|
601
604
|
ok: true,
|
|
602
|
-
version:
|
|
605
|
+
version: SERVER_VERSION,
|
|
603
606
|
api_version: "v3",
|
|
604
607
|
transport: "streamable-http",
|
|
605
608
|
sessions_count: count?.cnt ?? 0,
|
|
@@ -918,7 +921,7 @@ Bun.serve({
|
|
|
918
921
|
}
|
|
919
922
|
|
|
920
923
|
return withCors(req, new Response(
|
|
921
|
-
`CommHub MCP Server
|
|
924
|
+
`CommHub MCP Server v${SERVER_VERSION} (Streamable HTTP + SSE Push)
|
|
922
925
|
|
|
923
926
|
Endpoints:
|
|
924
927
|
POST /mcp - MCP Streamable HTTP (for Claude Code / Codex)
|
|
@@ -1030,7 +1033,7 @@ process.on("SIGINT", shutdown);
|
|
|
1030
1033
|
|
|
1031
1034
|
console.log(`
|
|
1032
1035
|
╔══════════════════════════════════════════════════╗
|
|
1033
|
-
║ CommHub MCP Server
|
|
1036
|
+
║ CommHub MCP Server v${SERVER_VERSION} ║
|
|
1034
1037
|
║ Transport: Streamable HTTP (Bun native) ║
|
|
1035
1038
|
║ Auth: ${AUTH_TOKEN ? "ENABLED (Bearer token)" : "DISABLED (set COMMHUB_AUTH_TOKEN)"}${"".padEnd(AUTH_TOKEN ? 5 : 0)}║
|
|
1036
1039
|
║ ║
|
package/src/tools.ts
CHANGED
|
@@ -12,13 +12,15 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
12
12
|
// If enforceNetworkId is set, override any client-supplied network_id
|
|
13
13
|
const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
|
|
14
14
|
|
|
15
|
-
// Check
|
|
16
|
-
// utok_ (no
|
|
17
|
-
|
|
15
|
+
// Check write access. For ntok_ the network is enforced by the token.
|
|
16
|
+
// For utok_ (no enforced network) we accept the network_id supplied in the
|
|
17
|
+
// request and verify the user has a write role on it.
|
|
18
|
+
const canWrite = (effectiveNetworkId?: string | null): boolean => {
|
|
18
19
|
if (!enforceUserId) return true; // legacy global token mode, allow
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const netId = enforceNetworkId ?? effectiveNetworkId ?? null;
|
|
21
|
+
if (!netId) return false; // no network resolvable
|
|
22
|
+
const role = getUserNetworkRole(enforceUserId, netId);
|
|
23
|
+
return !!role && role !== "viewer"; // owner/admin/member can write
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
const addScope = (sql: string, params: any[], networkId?: string | null, column = "network_id"): string => {
|
|
@@ -66,7 +68,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
66
68
|
},
|
|
67
69
|
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 }) => {
|
|
68
70
|
const effectiveNetId = getNetworkId(netId);
|
|
69
|
-
if (!canWrite()) {
|
|
71
|
+
if (!canWrite(effectiveNetId)) {
|
|
70
72
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
71
73
|
}
|
|
72
74
|
console.log(`[${ts()}] ${alias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}`);
|
|
@@ -177,7 +179,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
177
179
|
},
|
|
178
180
|
async ({ alias, task, result, artifacts, score, duration_minutes, network_id: netId }) => {
|
|
179
181
|
const effectiveNetId = getNetworkId(netId);
|
|
180
|
-
if (!canWrite()) {
|
|
182
|
+
if (!canWrite(effectiveNetId)) {
|
|
181
183
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
182
184
|
}
|
|
183
185
|
console.log(`[${ts()}] ${alias} → report_completion: ${task.slice(0, 60)}${effectiveNetId ? " [net]" : ""}`);
|
|
@@ -267,7 +269,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
267
269
|
},
|
|
268
270
|
async ({ alias, message_id, response }) => {
|
|
269
271
|
const effectiveNetId = getNetworkId(null);
|
|
270
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
272
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
271
273
|
console.log(`[${ts()}] ${alias} → ack_inbox: ${message_id.slice(0, 8)}`);
|
|
272
274
|
const ackParams: any[] = [message_id, alias];
|
|
273
275
|
let ackSql = "UPDATE inbox SET acked = 1 WHERE id = ?1 AND session_name = ?2";
|
|
@@ -391,7 +393,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
391
393
|
const effectiveNetId = getNetworkId(netId);
|
|
392
394
|
|
|
393
395
|
// Role check: viewer cannot send tasks
|
|
394
|
-
if (!canWrite()) {
|
|
396
|
+
if (!canWrite(effectiveNetId)) {
|
|
395
397
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied", message: "Viewer role cannot send tasks" }) }] };
|
|
396
398
|
}
|
|
397
399
|
|
|
@@ -458,7 +460,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
458
460
|
},
|
|
459
461
|
async ({ alias, message, from_session }) => {
|
|
460
462
|
const effectiveNetId = getNetworkId(null);
|
|
461
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
463
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
462
464
|
console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
|
|
463
465
|
const id = uuidv4();
|
|
464
466
|
db.run(
|
|
@@ -499,7 +501,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
499
501
|
},
|
|
500
502
|
async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
|
|
501
503
|
const effectiveNetId = getNetworkId(null);
|
|
502
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
504
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
503
505
|
console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
|
|
504
506
|
const id = uuidv4();
|
|
505
507
|
const replyLogged = db.transaction(() => {
|
|
@@ -550,7 +552,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
550
552
|
},
|
|
551
553
|
async ({ task_id, from_session }) => {
|
|
552
554
|
const effectiveNetId = getNetworkId(null);
|
|
553
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
555
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
554
556
|
console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
|
|
555
557
|
const updateParams: any[] = [task_id];
|
|
556
558
|
let updateSql = "UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status IN ('created', 'delivered')";
|
|
@@ -576,7 +578,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
576
578
|
},
|
|
577
579
|
async ({ task_id, from_session }) => {
|
|
578
580
|
const effectiveNetId = getNetworkId(null);
|
|
579
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
581
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
580
582
|
console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
|
|
581
583
|
// Find the original task
|
|
582
584
|
const taskParams: any[] = [task_id];
|
|
@@ -687,7 +689,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
687
689
|
},
|
|
688
690
|
async ({ task_id, reason, from_session }) => {
|
|
689
691
|
const effectiveNetId = getNetworkId(null);
|
|
690
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
692
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
691
693
|
console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
|
|
692
694
|
const updateParams: any[] = [reason || "cancelled by " + from_session, task_id];
|
|
693
695
|
let updateSql = `UPDATE tasks SET status = 'cancelled', result = ?1, completed_at = datetime('now')
|
|
@@ -719,7 +721,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
719
721
|
},
|
|
720
722
|
async ({ task_id, new_alias, from_session }) => {
|
|
721
723
|
const effectiveNetId = getNetworkId(null);
|
|
722
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
724
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
723
725
|
console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
|
|
724
726
|
const taskParams: any[] = [task_id];
|
|
725
727
|
let taskSql = "SELECT * FROM tasks WHERE task_id = ?1";
|
|
@@ -765,7 +767,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
765
767
|
},
|
|
766
768
|
async ({ message, filter_server, filter_status, network_id: netId }) => {
|
|
767
769
|
const effectiveNetId = getNetworkId(netId);
|
|
768
|
-
if (!canWrite()) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
770
|
+
if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
|
|
769
771
|
console.log(`[${ts()}] hub → broadcast: ${message.slice(0, 60)}${effectiveNetId ? " [net=" + effectiveNetId.slice(0, 12) + "]" : ""}`);
|
|
770
772
|
let sql = "SELECT alias, network_id FROM sessions WHERE alias IS NOT NULL";
|
|
771
773
|
const params: any[] = [];
|