@sleep2agi/commhub-server 0.8.0-preview.1 → 0.8.0

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.0-preview.1",
3
+ "version": "0.8.0",
4
4
  "description": "CommHub Server — 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
@@ -74,12 +74,12 @@ setInterval(() => {
74
74
  }, 300000);
75
75
 
76
76
  // ── Factory: 每个请求创建新的 McpServer(stateless 模式)──
77
- function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null): McpServer {
77
+ function createServer(clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false): McpServer {
78
78
  const server = new McpServer({
79
79
  name: "commhub",
80
80
  version: "0.5.0",
81
81
  });
82
- registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias);
82
+ registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias, callerTokenIsNetwork);
83
83
  return server;
84
84
  }
85
85
 
@@ -318,6 +318,7 @@ Bun.serve({
318
318
  // utok_ (user token, not network-bound) is allowed — the tool layer
319
319
  // scopes to the user's accessible networks. Without this Dashboard
320
320
  // (which logs in as a user) cannot call send_task.
321
+ const token = requestToken(req);
321
322
  const authCtx = resolveRequestAuth(req);
322
323
  const enforceNetId = authCtx?.networkId || null;
323
324
  // Derive the calling alias from the token name (e.g., 'node:视频审查')
@@ -328,7 +329,7 @@ Bun.serve({
328
329
  const transport = new WebStandardStreamableHTTPServerTransport({
329
330
  sessionIdGenerator: undefined,
330
331
  });
331
- const mcpServer = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias);
332
+ const mcpServer = createServer(clientIP, enforceNetId, authCtx?.userId || null, callerAlias, !!token?.startsWith("ntok_"));
332
333
  await mcpServer.connect(transport);
333
334
  const response = await transport.handleRequest(req);
334
335
  // Disconnect after response to prevent McpServer leak
@@ -343,6 +344,7 @@ Bun.serve({
343
344
  const authErr = requireAuth(req);
344
345
  if (authErr) return authErr;
345
346
  const sessionName = decodeURIComponent(eventsMatch[1]);
347
+ const token = requestToken(req);
346
348
  const authCtx = resolveRequestAuth(req);
347
349
  const scopedNetId = authCtx?.networkId || url.searchParams.get("network_id");
348
350
  if (!authCtx && isLegacyAuthToken(req)) {
@@ -355,7 +357,7 @@ Bun.serve({
355
357
  }
356
358
  return createSSEStream(sessionName, scopedNetId);
357
359
  }
358
- if (!authCtx || !scopedNetId) {
360
+ if (!token?.startsWith("ntok_") || !authCtx || !scopedNetId) {
359
361
  return withCors(req, Response.json({ ok: false, error: "network-scoped token required for SSE" }, { status: 403 }));
360
362
  }
361
363
  const role = getUserNetworkRole(authCtx.userId, scopedNetId);
@@ -755,7 +757,14 @@ Bun.serve({
755
757
  sql = addNetworkScope(sql, params, restScope);
756
758
  sql += " ORDER BY updated_at DESC";
757
759
  const sessions = db.all(sql, ...params);
758
- return withCors(req, Response.json({ ok: true, sessions }));
760
+ const summary = sessions.reduce((acc: any, session: any) => {
761
+ const raw = String(session.status || "").toLowerCase();
762
+ if (raw === "offline") acc.offline++;
763
+ else if (["working", "blocked", "error", "waiting_input", "running", "busy"].includes(raw)) acc.working++;
764
+ else acc.idle++;
765
+ return acc;
766
+ }, { idle: 0, working: 0, offline: 0, total: sessions.length });
767
+ return withCors(req, Response.json({ ok: true, sessions, summary }));
759
768
  }
760
769
 
761
770
  // ── REST: send task ──
package/src/tools.ts CHANGED
@@ -8,7 +8,7 @@ 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, callerAlias?: string | null) {
11
+ export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false) {
12
12
  // Default from_session for outbound tools — extracted from the calling
13
13
  // token's binding (ntok_ → node alias, utok_ → username). Without this,
14
14
  // an agent's send_task call always claimed from='hub' and peer agents
@@ -113,6 +113,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
113
113
  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 }) => {
114
114
  const effectiveNetId = getNetworkId(netId);
115
115
  const sessionNetId = effectiveNetId ?? "default";
116
+ if (!callerTokenIsNetwork || !enforceNetworkId) {
117
+ return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "network_token_required" }) }] };
118
+ }
116
119
  if (!canWrite(effectiveNetId)) {
117
120
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "permission_denied" }) }] };
118
121
  }