@sleep2agi/commhub-server 0.5.0-preview.33 → 0.5.0-preview.34
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 +4 -9
- 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.34",
|
|
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
|
@@ -228,17 +228,12 @@ Bun.serve({
|
|
|
228
228
|
if (authErr) return withCors(req, authErr);
|
|
229
229
|
const fwd = req.headers.get("x-forwarded-for");
|
|
230
230
|
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
|
|
231
|
+
// V3: resolve token → enforce network_id in all MCP tools.
|
|
232
|
+
// utok_ (user token, not network-bound) is allowed — the tool layer
|
|
233
|
+
// scopes to the user's accessible networks. Without this Dashboard
|
|
234
|
+
// (which logs in as a user) cannot call send_task.
|
|
232
235
|
const authCtx = resolveRequestAuth(req);
|
|
233
236
|
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
237
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
243
238
|
sessionIdGenerator: undefined,
|
|
244
239
|
});
|
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[] = [];
|