@sleep2agi/commhub-server 0.8.0-preview.1 → 0.8.0-preview.2
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 +14 -5
- package/src/tools.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.8.0-preview.
|
|
3
|
+
"version": "0.8.0-preview.2",
|
|
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
|
-
|
|
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
|
}
|