@sleep2agi/commhub-server 0.8.3 → 0.8.4-preview.1

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.8.3",
3
+ "version": "0.8.4-preview.1",
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/index.ts CHANGED
@@ -80,12 +80,12 @@ setInterval(() => {
80
80
  }, 300000);
81
81
 
82
82
  // ── Factory: 每个请求创建新的 McpServer(stateless 模式)──
83
- function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false): McpServer {
83
+ function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false, callerTokenId?: string | null): McpServer {
84
84
  const server = new McpServer({
85
85
  name: "commhub",
86
86
  version: "0.5.0",
87
87
  });
88
- registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias, callerTokenIsNetwork);
88
+ registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias, callerTokenIsNetwork, callerTokenId);
89
89
  return server;
90
90
  }
91
91
 
@@ -170,12 +170,12 @@ function requireTmuxAccess(req: Request, server?: any): Response | null {
170
170
  }
171
171
 
172
172
  // Extract user + network + token-binding identity from request token.
173
- function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string; tokenName: string | null } | null {
173
+ function resolveRequestAuth(req: Request): { userId: string; networkId: string | null; username: string; tokenName: string | null; tokenId: string | null } | null {
174
174
  const token = requestToken(req);
175
175
  if (!token) return null;
176
176
  const resolved = resolveToken(token);
177
177
  if (!resolved) return null;
178
- return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username, tokenName: resolved.tokenName };
178
+ return { userId: resolved.user.user_id, networkId: resolved.networkId, username: resolved.user.username, tokenName: resolved.tokenName, tokenId: resolved.tokenId };
179
179
  }
180
180
 
181
181
  type RestNetworkScope = {
@@ -449,7 +449,7 @@ Bun.serve({
449
449
  const transport = new WebStandardStreamableHTTPServerTransport({
450
450
  sessionIdGenerator: undefined,
451
451
  });
452
- const mcpServer = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias, !!token?.startsWith("ntok_"));
452
+ const mcpServer = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias, !!token?.startsWith("ntok_"), authCtx?.tokenId || null);
453
453
  await mcpServer.connect(transport);
454
454
  const response = await transport.handleRequest(req);
455
455
  // Disconnect after response to prevent McpServer leak
package/src/tools.ts CHANGED
@@ -19,13 +19,29 @@ function normalizeMetaJson(meta: unknown): string | null {
19
19
  try { return JSON.stringify(meta); } catch { return null; }
20
20
  }
21
21
 
22
- export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false) {
22
+ export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false, callerTokenId?: string | null) {
23
23
  // Default from_session for outbound tools — extracted from the calling
24
24
  // token's binding (ntok_ → node alias, utok_ → username). Without this,
25
25
  // an agent's send_task call always claimed from='hub' and peer agents
26
- // couldn't tell who actually asked them. Tool callers can still override
27
- // by passing from_session explicitly.
28
- const defaultFrom = (clientFrom?: string) => clientFrom || callerAlias || "hub";
26
+ // couldn't tell who actually asked them. Network-bound node tokens are an
27
+ // identity boundary: they must not spoof another node via from_session.
28
+ const defaultFrom = (clientFrom?: string) => (callerTokenIsNetwork && callerAlias) ? callerAlias : (clientFrom || callerAlias || "hub");
29
+ const fromIdentityMismatchReply = (clientFrom?: string) => {
30
+ const requestedFrom = clientFrom?.trim();
31
+ if (!callerTokenIsNetwork || !callerAlias || !requestedFrom || requestedFrom === callerAlias) return null;
32
+ return {
33
+ content: [{
34
+ type: "text" as const,
35
+ text: JSON.stringify({
36
+ ok: false,
37
+ error: "from_session_identity_mismatch",
38
+ message: "network token from_session does not match token-bound node alias",
39
+ token_alias: callerAlias,
40
+ requested_from_session: requestedFrom,
41
+ }),
42
+ }],
43
+ };
44
+ };
29
45
  // If enforceNetworkId is set, override any client-supplied network_id
30
46
  const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
31
47
 
@@ -190,6 +206,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
190
206
  }
191
207
  }
192
208
  console.log(`[${ts()}] ${effectiveAlias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}${canonical.renamed ? ` [renamed from ${alias}]` : ""}`);
209
+ if (callerTokenIsNetwork && callerTokenId) {
210
+ try {
211
+ db.run("UPDATE api_tokens SET name = ?1 WHERE token_id = ?2", [`node:${effectiveAlias}`, callerTokenId]);
212
+ } catch {}
213
+ }
193
214
  const trimmedOutput = output?.slice(0, 4000);
194
215
  const hostHostname = host?.hostname || hn || null;
195
216
  const hostIp = host?.ip || clientIP || null;
@@ -600,7 +621,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
600
621
  parent_task_id: z.string().max(200).optional().describe("Parent task this dispatch is on behalf of. When the child task replies the hub will auto-chain the answer to the parent task's originator, so the user sees the final result even if the intermediate session ends."),
601
622
  meta: z.any().optional().describe("Optional structured task metadata, e.g. { attachments: [{ type, path, url, mime, name, size }] }."),
602
623
  },
603
- async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn, meta }) => { const from_session = defaultFrom(_fromIn);
624
+ async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn, meta }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
604
625
  const effectiveNetId = getNetworkId(netId);
605
626
  const metaJson = normalizeMetaJson(meta);
606
627
  // Resolve parent_task_id: explicit > inferred (caller's most recent
@@ -699,7 +720,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
699
720
  message: z.string().min(1).max(10000).describe("Message content"),
700
721
  from_session: z.string().max(200).optional(),
701
722
  },
702
- async ({ alias, message, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
723
+ async ({ alias, message, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
703
724
  const effectiveNetId = getNetworkId(null);
704
725
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
705
726
  console.log(`[${ts()}] ${from_session} → send_message → ${alias}: ${message.slice(0, 60)}`);
@@ -740,7 +761,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
740
761
  status: z.enum(["replied", "failed", "cancelled"]).optional().default("replied").describe("Task outcome"),
741
762
  from_session: z.string().max(200).optional(),
742
763
  },
743
- async ({ alias, text, in_reply_to, status: replyStatus, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
764
+ async ({ alias, text, in_reply_to, status: replyStatus, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
744
765
  const effectiveNetId = getNetworkId(null);
745
766
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
746
767
  console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
@@ -815,7 +836,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
815
836
  task_id: z.string().min(1).max(200).describe("Task ID to acknowledge"),
816
837
  from_session: z.string().max(200).optional(),
817
838
  },
818
- async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
839
+ async ({ task_id, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
819
840
  const effectiveNetId = getNetworkId(null);
820
841
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
821
842
  console.log(`[${ts()}] ${from_session} → send_ack → task ${task_id.slice(0, 8)}`);
@@ -841,7 +862,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
841
862
  task_id: z.string().min(1).max(200).describe("Task ID to retry"),
842
863
  from_session: z.string().max(200).optional(),
843
864
  },
844
- async ({ task_id, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
865
+ async ({ task_id, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
845
866
  const effectiveNetId = getNetworkId(null);
846
867
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
847
868
  console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
@@ -952,7 +973,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
952
973
  reason: z.string().max(1000).optional().describe("Cancellation reason"),
953
974
  from_session: z.string().max(200).optional(),
954
975
  },
955
- async ({ task_id, reason, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
976
+ async ({ task_id, reason, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
956
977
  const effectiveNetId = getNetworkId(null);
957
978
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
958
979
  console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
@@ -984,7 +1005,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
984
1005
  new_alias: z.string().min(1).max(200).describe("Target agent alias"),
985
1006
  from_session: z.string().max(200).optional(),
986
1007
  },
987
- async ({ task_id, new_alias, from_session: _fromIn }) => { const from_session = defaultFrom(_fromIn);
1008
+ async ({ task_id, new_alias, from_session: _fromIn }) => { const fromMismatch = fromIdentityMismatchReply(_fromIn); if (fromMismatch) return fromMismatch; const from_session = defaultFrom(_fromIn);
988
1009
  const effectiveNetId = getNetworkId(null);
989
1010
  if (!canWrite(effectiveNetId)) return writeDeniedReply(effectiveNetId);
990
1011
  console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);