@sleep2agi/commhub-server 0.5.0-preview.37 → 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.37",
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
@@ -462,9 +468,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
462
468
  {
463
469
  alias: z.string().min(1).max(200).describe("Target session alias"),
464
470
  message: z.string().min(1).max(10000).describe("Message content"),
465
- from_session: z.string().max(200).optional().default("hub"),
471
+ from_session: z.string().max(200).optional(),
466
472
  },
467
- async ({ alias, message, from_session }) => {
473
+ async ({ alias, message, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
468
474
  const effectiveNetId = getNetworkId(null);
469
475
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
470
476
  console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
@@ -503,9 +509,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
503
509
  text: z.string().min(1).max(10000).describe("Reply content"),
504
510
  in_reply_to: z.string().max(200).optional().describe("Original task/message ID"),
505
511
  status: z.enum(["replied", "failed", "cancelled"]).optional().default("replied").describe("Task outcome"),
506
- from_session: z.string().max(200).optional().default("hub"),
512
+ from_session: z.string().max(200).optional(),
507
513
  },
508
- 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);
509
515
  const effectiveNetId = getNetworkId(null);
510
516
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
511
517
  console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
@@ -554,9 +560,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
554
560
  "Acknowledge receipt of a task. Does NOT enter inbox. Updates task status only.",
555
561
  {
556
562
  task_id: z.string().min(1).max(200).describe("Task ID to acknowledge"),
557
- from_session: z.string().max(200).optional().default("hub"),
563
+ from_session: z.string().max(200).optional(),
558
564
  },
559
- async ({ task_id, from_session }) => {
565
+ async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
560
566
  const effectiveNetId = getNetworkId(null);
561
567
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
562
568
  console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
@@ -580,9 +586,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
580
586
  "Retry a failed, expired, or cancelled task. Resets status to delivered and re-queues in inbox.",
581
587
  {
582
588
  task_id: z.string().min(1).max(200).describe("Task ID to retry"),
583
- from_session: z.string().max(200).optional().default("hub"),
589
+ from_session: z.string().max(200).optional(),
584
590
  },
585
- async ({ task_id, from_session }) => {
591
+ async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
586
592
  const effectiveNetId = getNetworkId(null);
587
593
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
588
594
  console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
@@ -689,9 +695,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
689
695
  {
690
696
  task_id: z.string().min(1).max(200).describe("Task ID to cancel"),
691
697
  reason: z.string().max(1000).optional().describe("Cancellation reason"),
692
- from_session: z.string().max(200).optional().default("hub"),
698
+ from_session: z.string().max(200).optional(),
693
699
  },
694
- async ({ task_id, reason, from_session }) => {
700
+ async ({ task_id, reason, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
695
701
  const effectiveNetId = getNetworkId(null);
696
702
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
697
703
  console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
@@ -721,9 +727,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
721
727
  {
722
728
  task_id: z.string().min(1).max(200).describe("Task ID to reassign"),
723
729
  new_alias: z.string().min(1).max(200).describe("Target agent alias"),
724
- from_session: z.string().max(200).optional().default("hub"),
730
+ from_session: z.string().max(200).optional(),
725
731
  },
726
- async ({ task_id, new_alias, from_session }) => {
732
+ async ({ task_id, new_alias, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
727
733
  const effectiveNetId = getNetworkId(null);
728
734
  if (!canWrite(effectiveNetId)) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
729
735
  console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);