@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 +1 -1
- package/src/index.ts +5 -5
- package/src/tools.ts +32 -11
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.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.
|
|
27
|
-
//
|
|
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}`);
|