@sleep2agi/commhub-server 0.5.0-preview.36 → 0.5.0-preview.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.5.0-preview.36",
3
+ "version": "0.5.0-preview.38",
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/auth.ts CHANGED
@@ -126,10 +126,11 @@ export function createNetworkTokenForNode(userId: string, networkId: string, nod
126
126
  return { ok: true, token };
127
127
  }
128
128
 
129
- export function resolveToken(token: string): { user: AuthUser; networkId: string | null } | null {
129
+ export function resolveToken(token: string): { user: AuthUser; networkId: string | null; tokenName: string | null } | null {
130
130
  const tHash = hashToken(token);
131
131
  const row = db.get<any>(
132
- `SELECT t.user_id, t.network_id, t.scope, u.username, u.display_name, u.email, u.role
132
+ `SELECT t.user_id, t.network_id, t.scope, t.name AS token_name,
133
+ u.username, u.display_name, u.email, u.role
133
134
  FROM api_tokens t JOIN users u ON t.user_id = u.user_id
134
135
  WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > datetime('now'))`,
135
136
  tHash);
@@ -142,6 +143,11 @@ export function resolveToken(token: string): { user: AuthUser; networkId: string
142
143
  return {
143
144
  user: { user_id: row.user_id, username: row.username, display_name: row.display_name, email: row.email, role: row.role },
144
145
  networkId: row.network_id,
146
+ // tokenName carries the binding identity. For node-scoped ntok_, it's
147
+ // 'node:<alias>'; we strip the prefix and use it as the default
148
+ // from_session for any MCP send_task / send_message / etc, so peer
149
+ // agents see who actually called them (not 'hub').
150
+ tokenName: row.token_name || null,
145
151
  };
146
152
  }
147
153
 
package/src/index.ts CHANGED
@@ -42,12 +42,12 @@ setInterval(() => {
42
42
  }, 300000);
43
43
 
44
44
  // ── Factory: 每个请求创建新的 McpServer(stateless 模式)──
45
- function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null): McpServer {
45
+ function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null): McpServer {
46
46
  const server = new McpServer({
47
47
  name: "commhub",
48
48
  version: "0.5.0",
49
49
  });
50
- registerTools(server, clientIP, enforceNetworkId, enforceUserId);
50
+ registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias);
51
51
  return server;
52
52
  }
53
53
 
@@ -70,15 +70,15 @@ function requireAuth(req: Request): Response | null {
70
70
  return Response.json({ error: "unauthorized" }, { status: 401 });
71
71
  }
72
72
 
73
- // Extract user + network from request token (for authorization)
74
- function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string } | null {
73
+ // Extract user + network + token-binding identity from request token.
74
+ function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string; tokenName: string | null } | null {
75
75
  const header = req.headers.get("Authorization")?.replace("Bearer ", "");
76
76
  const url = new URL(req.url);
77
77
  const token = header || url.searchParams.get("token") || "";
78
78
  if (!token) return null;
79
79
  const resolved = resolveToken(token);
80
80
  if (!resolved) return null;
81
- return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username };
81
+ return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username, tokenName: resolved.tokenName };
82
82
  }
83
83
 
84
84
  type RestNetworkScope = {
@@ -242,10 +242,15 @@ Bun.serve({
242
242
  // (which logs in as a user) cannot call send_task.
243
243
  const authCtx = resolveRequestAuth(req);
244
244
  const enforceNetId = authCtx?.networkId || null;
245
+ // Derive the calling alias from the token name (e.g., 'node:视频审查')
246
+ // so peer agents see the real sender instead of 'hub' on send_task.
247
+ const callerAlias = authCtx?.tokenName?.startsWith("node:")
248
+ ? authCtx.tokenName.slice("node:".length)
249
+ : (authCtx?.username || null);
245
250
  const transport = new WebStandardStreamableHTTPServerTransport({
246
251
  sessionIdGenerator: undefined,
247
252
  });
248
- const server = createServer(clientIP, enforceNetId, authCtx?.userId || null);
253
+ const server = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias);
249
254
  await server.connect(transport);
250
255
  const response = await transport.handleRequest(req);
251
256
  // Disconnect after response to prevent McpServer leak
package/src/tools.ts CHANGED
@@ -8,7 +8,13 @@ function ts(): string {
8
8
  return new Date().toTimeString().slice(0, 8);
9
9
  }
10
10
 
11
- export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null) {
11
+ export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null) {
12
+ // Default from_session for outbound tools — extracted from the calling
13
+ // token's binding (ntok_ → node alias, utok_ → username). Without this,
14
+ // an agent's send_task call always claimed from='hub' and peer agents
15
+ // couldn't tell who actually asked them. Tool callers can still override
16
+ // by passing from_session explicitly.
17
+ const defaultFrom = (clientFrom?: string) => clientFrom || callerAlias || "hub";
12
18
  // If enforceNetworkId is set, override any client-supplied network_id
13
19
  const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
14
20
 
@@ -385,11 +391,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
385
391
  task: z.string().min(1).max(10000).describe("Task content"),
386
392
  priority: z.enum(["high", "normal", "low"]).optional().default("normal"),
387
393
  context: z.string().max(10000).optional(),
388
- from_session: z.string().max(200).optional().default("hub"),
394
+ from_session: z.string().max(200).optional(),
389
395
  ttl_seconds: z.number().min(1).max(86400).optional().describe("Task TTL in seconds (default: 3600)"),
390
396
  network_id: z.string().max(200).optional().describe("Network scope"),
391
397
  },
392
- async ({ alias, task, priority, context, from_session, ttl_seconds, network_id: netId }) => {
398
+ async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId }) => { const from_session = defaultFrom(_fromIn);
393
399
  const effectiveNetId = getNetworkId(netId);
394
400
 
395
401
  // Role check: viewer cannot send tasks
@@ -428,12 +434,18 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
428
434
 
429
435
  const session = scopedSessionStatus(alias, effectiveNetId);
430
436
 
431
- // SSE push by alias
437
+ // SSE push by alias.
438
+ // The SSE channel is keyed by alias (subscribers connected to /events/<alias>),
439
+ // not by network_id. Earlier we gated the push on a network-scoped session
440
+ // lookup, which silently dropped pushes whenever an agent registered with
441
+ // network_id=null but the sender supplied an explicit network_id (the
442
+ // exact mismatch hit by Dashboard tasks). Push unconditionally; the
443
+ // subscriber's own auth (ntok_) constrains who can listen.
432
444
  const pendingParams: any[] = [alias];
433
445
  let pendingSql = "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0";
434
446
  pendingSql = addScope(pendingSql, pendingParams, effectiveNetId);
435
447
  const pending = db.get<{ cnt: number }>(pendingSql, ...pendingParams);
436
- if (session) pushEvent(alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session });
448
+ pushEvent(alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session });
437
449
 
438
450
  return {
439
451
  content: [
@@ -456,9 +468,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
456
468
  {
457
469
  alias: z.string().min(1).max(200).describe("Target session alias"),
458
470
  message: z.string().min(1).max(10000).describe("Message content"),
459
- from_session: z.string().max(200).optional().default("hub"),
471
+ from_session: z.string().max(200).optional(),
460
472
  },
461
- async ({ alias, message, from_session }) => {
473
+ async ({ alias, message, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
462
474
  const effectiveNetId = getNetworkId(null);
463
475
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
464
476
  console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
@@ -471,7 +483,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
471
483
 
472
484
  const session = scopedSessionStatus(alias, effectiveNetId);
473
485
 
474
- if (session) pushEvent(alias, { type: "new_message", message, from: from_session, message_id: id });
486
+ pushEvent(alias, { type: "new_message", message, from: from_session, message_id: id });
475
487
 
476
488
  return {
477
489
  content: [
@@ -497,9 +509,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
497
509
  text: z.string().min(1).max(10000).describe("Reply content"),
498
510
  in_reply_to: z.string().max(200).optional().describe("Original task/message ID"),
499
511
  status: z.enum(["replied", "failed", "cancelled"]).optional().default("replied").describe("Task outcome"),
500
- from_session: z.string().max(200).optional().default("hub"),
512
+ from_session: z.string().max(200).optional(),
501
513
  },
502
- async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
514
+ async ({ alias, text, in_reply_to, status: replyStatus, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
503
515
  const effectiveNetId = getNetworkId(null);
504
516
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
505
517
  console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
@@ -531,7 +543,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
531
543
  if (replyLogged && in_reply_to) logTaskEvent(in_reply_to, null, replyStatus, from_session, text.slice(0, 200));
532
544
 
533
545
  const session = scopedSessionStatus(alias, effectiveNetId);
534
- if (session) pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus });
546
+ pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus });
535
547
 
536
548
  return {
537
549
  content: [{
@@ -548,9 +560,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
548
560
  "Acknowledge receipt of a task. Does NOT enter inbox. Updates task status only.",
549
561
  {
550
562
  task_id: z.string().min(1).max(200).describe("Task ID to acknowledge"),
551
- from_session: z.string().max(200).optional().default("hub"),
563
+ from_session: z.string().max(200).optional(),
552
564
  },
553
- async ({ task_id, from_session }) => {
565
+ async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
554
566
  const effectiveNetId = getNetworkId(null);
555
567
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
556
568
  console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
@@ -574,9 +586,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
574
586
  "Retry a failed, expired, or cancelled task. Resets status to delivered and re-queues in inbox.",
575
587
  {
576
588
  task_id: z.string().min(1).max(200).describe("Task ID to retry"),
577
- from_session: z.string().max(200).optional().default("hub"),
589
+ from_session: z.string().max(200).optional(),
578
590
  },
579
- async ({ task_id, from_session }) => {
591
+ async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
580
592
  const effectiveNetId = getNetworkId(null);
581
593
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
582
594
  console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
@@ -607,10 +619,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
607
619
  );
608
620
  });
609
621
  logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
610
- // SSE push
611
- if (scopedSessionStatus(task.to_name, effectiveNetId ?? task.network_id)) {
612
- pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
613
- }
622
+ // SSE push (unconditional — channel is keyed by alias, not network)
623
+ pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
614
624
  return {
615
625
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, retried_to: task.to_name }) }],
616
626
  };
@@ -685,9 +695,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
685
695
  {
686
696
  task_id: z.string().min(1).max(200).describe("Task ID to cancel"),
687
697
  reason: z.string().max(1000).optional().describe("Cancellation reason"),
688
- from_session: z.string().max(200).optional().default("hub"),
698
+ from_session: z.string().max(200).optional(),
689
699
  },
690
- async ({ task_id, reason, from_session }) => {
700
+ async ({ task_id, reason, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
691
701
  const effectiveNetId = getNetworkId(null);
692
702
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
693
703
  console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
@@ -717,9 +727,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
717
727
  {
718
728
  task_id: z.string().min(1).max(200).describe("Task ID to reassign"),
719
729
  new_alias: z.string().min(1).max(200).describe("Target agent alias"),
720
- from_session: z.string().max(200).optional().default("hub"),
730
+ from_session: z.string().max(200).optional(),
721
731
  },
722
- async ({ task_id, new_alias, from_session }) => {
732
+ async ({ task_id, new_alias, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
723
733
  const effectiveNetId = getNetworkId(null);
724
734
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
725
735
  console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
@@ -749,9 +759,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
749
759
  [newInboxId, new_alias, task.priority, task.content, from_session, effectiveNetId ?? task.network_id ?? null]);
750
760
  });
751
761
  logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${new_alias}`);
752
- if (scopedSessionStatus(new_alias, effectiveNetId ?? task.network_id)) {
753
- pushEvent(new_alias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
754
- }
762
+ pushEvent(new_alias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
755
763
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: new_alias }) }] };
756
764
  }
757
765
  );