@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 +1 -1
- package/src/db.ts +2 -0
- package/src/index.ts +19 -11
- package/src/tools.ts +33 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.8.
|
|
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
|
-
|
|
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.
|
|
17
|
-
//
|
|
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";
|