@sleep2agi/commhub-server 0.8.3-preview.2 → 0.8.4-preview.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.3-preview.2",
3
+ "version": "0.8.4-preview.0",
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/db.ts CHANGED
@@ -129,6 +129,7 @@ for (const col of [
129
129
  { name: "requires_response", def: "TEXT DEFAULT 'reply'" },
130
130
  { name: "expires_at", def: "TEXT" },
131
131
  { name: "scope", def: "TEXT DEFAULT 'single'" },
132
+ { name: "meta_json", def: "TEXT" },
132
133
  ]) {
133
134
  try { db.exec(`ALTER TABLE inbox ADD COLUMN ${col.name} ${col.def}`); } catch {}
134
135
  }
@@ -489,6 +490,7 @@ try { db.exec("CREATE INDEX IF NOT EXISTS idx_completions_network ON completions
489
490
  // admin to see the answer even if 指挥室's own session has died. The hub forwards
490
491
  // the reply up the chain via parent_task_id.
491
492
  try { db.exec("ALTER TABLE tasks ADD COLUMN parent_task_id TEXT"); } catch {}
493
+ try { db.exec("ALTER TABLE tasks ADD COLUMN meta_json TEXT"); } catch {}
492
494
  try { db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id)"); } catch {}
493
495
 
494
496
  // Helpers
package/src/index.ts CHANGED
@@ -51,6 +51,11 @@ console.info = (...args: any[]) => { pushLog("info", args); _origConsole.info(..
51
51
  console.warn = (...args: any[]) => { pushLog("warn", args); _origConsole.warn(...args); };
52
52
  console.error = (...args: any[]) => { pushLog("error", args); _origConsole.error(...args); };
53
53
 
54
+ function normalizeMetaJson(meta: unknown): string | null {
55
+ if (!meta || typeof meta !== "object") return null;
56
+ try { return JSON.stringify(meta); } catch { return null; }
57
+ }
58
+
54
59
  // ── Rate limiter (in-memory, per IP) ──
55
60
  const rateLimits = new Map<string, { count: number; resetAt: number }>();
56
61
  function checkRateLimit(ip: string, maxPerMinute = 60): boolean {
@@ -75,12 +80,12 @@ setInterval(() => {
75
80
  }, 300000);
76
81
 
77
82
  // ── Factory: 每个请求创建新的 McpServer(stateless 模式)──
78
- 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 {
79
84
  const server = new McpServer({
80
85
  name: "commhub",
81
86
  version: "0.5.0",
82
87
  });
83
- registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias, callerTokenIsNetwork);
88
+ registerTools(server, clientIP, enforceNetworkId, enforceUserId, callerAlias, callerTokenIsNetwork, callerTokenId);
84
89
  return server;
85
90
  }
86
91
 
@@ -165,12 +170,12 @@ function requireTmuxAccess(req: Request, server?: any): Response | null {
165
170
  }
166
171
 
167
172
  // Extract user + network + token-binding identity from request token.
168
- 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 {
169
174
  const token = requestToken(req);
170
175
  if (!token) return null;
171
176
  const resolved = resolveToken(token);
172
177
  if (!resolved) return null;
173
- 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 };
174
179
  }
175
180
 
176
181
  type RestNetworkScope = {
@@ -343,6 +348,8 @@ const TaskSchema = z.object({
343
348
  from: z.string().max(200).optional(),
344
349
  network_id: z.string().max(200).optional(),
345
350
  parent_task_id: z.string().max(200).optional(),
351
+ ttl_seconds: z.number().min(1).max(86400).optional(),
352
+ meta: z.any().optional(),
346
353
  });
347
354
 
348
355
  const BroadcastSchema = z.object({
@@ -442,7 +449,7 @@ Bun.serve({
442
449
  const transport = new WebStandardStreamableHTTPServerTransport({
443
450
  sessionIdGenerator: undefined,
444
451
  });
445
- 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);
446
453
  await mcpServer.connect(transport);
447
454
  const response = await transport.handleRequest(req);
448
455
  // Disconnect after response to prevent McpServer leak
@@ -1194,6 +1201,7 @@ Bun.serve({
1194
1201
  const id = crypto.randomUUID();
1195
1202
  const fromSession = body.from || "api";
1196
1203
  const ttlSeconds = (body as any).ttl_seconds || 3600;
1204
+ const metaJson = normalizeMetaJson((body as any).meta);
1197
1205
  // Mirror send_task MCP: write inbox + tasks rows in a single
1198
1206
  // transaction so the dispatch is visible to dashboard's Tasks page
1199
1207
  // and the parent_task_id lineage chain. Previously this endpoint
@@ -1201,14 +1209,14 @@ Bun.serve({
1201
1209
  // dispatched via REST (anet demo, dashboard Dispatch button, etc.).
1202
1210
  db.transaction(() => {
1203
1211
  db.run(
1204
- `INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response, network_id)
1205
- VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply', ?6)`,
1206
- [id, targetAlias, body.priority, body.task, fromSession, taskNetId]
1212
+ `INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response, network_id, meta_json)
1213
+ VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply', ?6, ?7)`,
1214
+ [id, targetAlias, body.priority, body.task, fromSession, taskNetId, metaJson]
1207
1215
  );
1208
1216
  db.run(
1209
- `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id)
1210
- VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8)`,
1211
- [id, fromSession, targetAlias, body.priority, body.task, `+${ttlSeconds} seconds`, taskNetId, body.parent_task_id ?? null]
1217
+ `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id, meta_json)
1218
+ VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8, ?9)`,
1219
+ [id, fromSession, targetAlias, body.priority, body.task, `+${ttlSeconds} seconds`, taskNetId, body.parent_task_id ?? null, metaJson]
1212
1220
  );
1213
1221
  // Touch session row so the dashboard reflects "task in flight"
1214
1222
  // immediately, without waiting for the agent's report_status to
package/src/tools.ts CHANGED
@@ -9,13 +9,23 @@ function ts(): string {
9
9
  return new Date().toTimeString().slice(0, 8);
10
10
  }
11
11
 
12
- export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false) {
12
+ function parseMetaJson(value: unknown): unknown | null {
13
+ if (!value || typeof value !== "string") return null;
14
+ try { return JSON.parse(value); } catch { return null; }
15
+ }
16
+
17
+ function normalizeMetaJson(meta: unknown): string | null {
18
+ if (!meta || typeof meta !== "object") return null;
19
+ try { return JSON.stringify(meta); } catch { return null; }
20
+ }
21
+
22
+ export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false, callerTokenId?: string | null) {
13
23
  // Default from_session for outbound tools — extracted from the calling
14
24
  // token's binding (ntok_ → node alias, utok_ → username). Without this,
15
25
  // an agent's send_task call always claimed from='hub' and peer agents
16
- // couldn't tell who actually asked them. Tool callers can still override
17
- // by passing from_session explicitly.
18
- 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");
19
29
  // If enforceNetworkId is set, override any client-supplied network_id
20
30
  const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
21
31
 
@@ -180,6 +190,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
180
190
  }
181
191
  }
182
192
  console.log(`[${ts()}] ${effectiveAlias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}${canonical.renamed ? ` [renamed from ${alias}]` : ""}`);
193
+ if (callerTokenIsNetwork && callerTokenId) {
194
+ try {
195
+ db.run("UPDATE api_tokens SET name = ?1 WHERE token_id = ?2", [`node:${effectiveAlias}`, callerTokenId]);
196
+ } catch {}
197
+ }
183
198
  const trimmedOutput = output?.slice(0, 4000);
184
199
  const hostHostname = host?.hostname || hn || null;
185
200
  const hostIp = host?.ip || clientIP || null;
@@ -439,13 +454,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
439
454
  const rows0 = db.get<{ cnt: number }>(countSql, ...countParams);
440
455
  console.log(`[${ts()}] ${alias} → get_inbox: ${rows0?.cnt ?? 0} pending messages`);
441
456
  const rowsParams: any[] = [alias];
442
- let rowsSql = `SELECT id, type, priority, content, context, from_session, created_at, network_id
457
+ let rowsSql = `SELECT id, type, priority, content, context, from_session, created_at, network_id, meta_json
443
458
  FROM inbox WHERE session_name = ?1 AND acked = 0`;
444
459
  rowsSql = addReadScope(rowsSql, rowsParams, readScope);
445
460
  rowsSql += ` ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_at
446
461
  LIMIT ?${rowsParams.length + 1}`;
447
462
  rowsParams.push(limit);
448
- const rows = db.all(rowsSql, ...rowsParams);
463
+ const rows = db.all(rowsSql, ...rowsParams).map((row: any) => ({
464
+ ...row,
465
+ meta: parseMetaJson(row.meta_json),
466
+ }));
449
467
 
450
468
  return {
451
469
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, messages: rows }) }],
@@ -585,9 +603,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
585
603
  ttl_seconds: z.number().min(1).max(86400).optional().describe("Task TTL in seconds (default: 3600)"),
586
604
  network_id: z.string().max(200).optional().describe("Network scope"),
587
605
  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."),
606
+ meta: z.any().optional().describe("Optional structured task metadata, e.g. { attachments: [{ type, path, url, mime, name, size }] }."),
588
607
  },
589
- async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn }) => { const from_session = defaultFrom(_fromIn);
608
+ async ({ alias, task, priority, context, from_session: _fromIn, ttl_seconds, network_id: netId, parent_task_id: parentIn, meta }) => { const from_session = defaultFrom(_fromIn);
590
609
  const effectiveNetId = getNetworkId(netId);
610
+ const metaJson = normalizeMetaJson(meta);
591
611
  // Resolve parent_task_id: explicit > inferred (caller's most recent
592
612
  // delivered/started inbox task that's still open). Inference is the
593
613
  // safety net for when the LLM forgets to pass parent_task_id.
@@ -629,14 +649,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
629
649
  // 报告冲突)。
630
650
  db.transaction(() => {
631
651
  db.run(
632
- `INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id)
633
- VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7)`,
634
- [id, targetAlias, priority, task, context ?? null, from_session, effectiveNetId ?? null]
652
+ `INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id, meta_json)
653
+ VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7, ?8)`,
654
+ [id, targetAlias, priority, task, context ?? null, from_session, effectiveNetId ?? null, metaJson]
635
655
  );
636
656
  db.run(
637
- `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id)
638
- VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8)`,
639
- [id, from_session, targetAlias, priority, task, `+${ttl_seconds || 3600} seconds`, effectiveNetId ?? null, parentTaskId]
657
+ `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id, meta_json)
658
+ VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8, ?9)`,
659
+ [id, from_session, targetAlias, priority, task, `+${ttl_seconds || 3600} seconds`, effectiveNetId ?? null, parentTaskId, metaJson]
640
660
  );
641
661
  const touchParams: any[] = [task.slice(0, 200), targetAlias];
642
662
  let touchSql = "UPDATE sessions SET task = ?1, updated_at = datetime('now') WHERE alias = ?2";