@sleep2agi/commhub-server 0.9.0-preview.21 → 0.9.0-preview.23
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/README.md +1 -1
- package/bin/commhub.ts +6 -2
- package/package.json +1 -1
- package/src/api-host-supervisors-fallback.test.ts +15 -5
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/avatar-validate.test.ts +113 -0
- package/src/avatar-validate.ts +69 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/db.ts +42 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +349 -0
- package/src/health-redaction.test.ts +176 -0
- package/src/index.ts +11 -2385
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +213 -0
- package/src/observer-avatar-http.test.ts +349 -0
- package/src/observer-push.test.ts +176 -0
- package/src/push.ts +87 -7
- package/src/response-charset.test.ts +130 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/server.ts +2831 -0
- package/src/tools.ts +26 -3
- package/src/uploads-http.test.ts +11 -8
package/src/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
3
|
import { db, uuidv4, logTaskEvent, chainReplyToParent, hashToken, generateId, generateNetworkToken } from "./db.js";
|
|
4
|
-
import { pushEvent } from "./push.js";
|
|
4
|
+
import { pushEvent, pushNetworkObserverEvent } from "./push.js";
|
|
5
5
|
import { assertNodeActive } from "./lifecycle-guard.js";
|
|
6
6
|
import { getUserNetworkRole, createNetworkTokenForNode } from "./auth.js";
|
|
7
7
|
import {
|
|
@@ -958,6 +958,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
958
958
|
if (target.state === "online") {
|
|
959
959
|
pushEvent(targetAlias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session, ...(canonical.renamed ? { renamed_from: alias } : {}) }, effectiveNetId);
|
|
960
960
|
}
|
|
961
|
+
// #461 network observer summary — unconditional (task row exists
|
|
962
|
+
// even when the target is offline/queued), metadata only.
|
|
963
|
+
pushNetworkObserverEvent(effectiveNetId, { type: "new_task", task_id: id, from: from_session, to: targetAlias, status: target.state === "online" ? "delivered" : "queued", priority });
|
|
961
964
|
|
|
962
965
|
if (target.state === "offline") {
|
|
963
966
|
return {
|
|
@@ -1055,7 +1058,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1055
1058
|
// ── V2: send_reply (关联 task_id,不触发 think) ──
|
|
1056
1059
|
server.tool(
|
|
1057
1060
|
"send_reply",
|
|
1058
|
-
"Send a reply to a task.
|
|
1061
|
+
"Send a reply to a Dashboard/UI-originated task (updates task row, emits new_reply SSE — Dashboard reads it). ⚠ NOT for agent-to-agent replies: agent nodes only log new_reply and do not processInbox on it, so the peer will not see the reply in real time. Use send_task for peer-to-peer replies (RFC-030, Vincent 2026-07-28 全网规则). If the target alias resolves to a live agent node, the response includes a `warning` field pointing to send_task.",
|
|
1059
1062
|
{
|
|
1060
1063
|
alias: z.string().min(1).max(200).describe("Target session alias"),
|
|
1061
1064
|
text: z.string().min(1).max(10000).describe("Reply content"),
|
|
@@ -1185,11 +1188,29 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1185
1188
|
|
|
1186
1189
|
const session = scopedSessionStatus(alias, effectiveNetId);
|
|
1187
1190
|
pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus }, effectiveNetId);
|
|
1191
|
+
// #461 network observer summary — ids + routing only, no reply text.
|
|
1192
|
+
pushNetworkObserverEvent(effectiveNetId, { type: "new_reply", task_id: in_reply_to ?? null, message_id: id, from: from_session, to: alias, status: replyStatus });
|
|
1193
|
+
|
|
1194
|
+
// #498 — When the reply target is a live agent node (not Dashboard/UI),
|
|
1195
|
+
// warn the caller that agent peers only log new_reply and do not
|
|
1196
|
+
// processInbox on it. Points at the correct next action (send_task)
|
|
1197
|
+
// so a follow-up turn corrects course rather than "why didn't they
|
|
1198
|
+
// respond" spelunking. Absent field when target is Dashboard/hub/api
|
|
1199
|
+
// (the correct path for commhub_reply) — silence is affirmation.
|
|
1200
|
+
const targetIsAgent = replyTargetNodeId !== null && alias !== "hub" && alias !== "api";
|
|
1201
|
+
const warning = targetIsAgent
|
|
1202
|
+
? `Target "${alias}" is an agent node. commhub_reply/send_reply does NOT wake agent peers (they only log new_reply, they do not processInbox on it) — the peer will not see this reply in real time. For agent-to-agent replies, use commhub_send_task(alias="${alias}", task="<your reply>") so the peer wakes via new_task SSE and processes it. (RFC-030, Vincent 2026-07-28 全网规则.)`
|
|
1203
|
+
: undefined;
|
|
1188
1204
|
|
|
1189
1205
|
return {
|
|
1190
1206
|
content: [{
|
|
1191
1207
|
type: "text" as const,
|
|
1192
|
-
text: JSON.stringify({
|
|
1208
|
+
text: JSON.stringify({
|
|
1209
|
+
ok: true,
|
|
1210
|
+
message_id: id,
|
|
1211
|
+
session_status: session?.status ?? "unknown",
|
|
1212
|
+
...(warning ? { warning } : {}),
|
|
1213
|
+
}),
|
|
1193
1214
|
}],
|
|
1194
1215
|
};
|
|
1195
1216
|
}
|
|
@@ -1267,6 +1288,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1267
1288
|
logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
|
|
1268
1289
|
// SSE push (unconditional — channel is keyed by alias, not network)
|
|
1269
1290
|
pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session }, effectiveNetId ?? task.network_id ?? null);
|
|
1291
|
+
pushNetworkObserverEvent(effectiveNetId ?? task.network_id ?? null, { type: "new_task", task_id, from: from_session, to: task.to_name, status: "delivered", priority: task.priority });
|
|
1270
1292
|
return {
|
|
1271
1293
|
content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, retried_to: task.to_name }) }],
|
|
1272
1294
|
};
|
|
@@ -1419,6 +1441,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1419
1441
|
});
|
|
1420
1442
|
logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${reassignedAlias}`);
|
|
1421
1443
|
pushEvent(reassignedAlias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session, ...(canonical.renamed ? { renamed_from: new_alias } : {}) }, effectiveNetId ?? task.network_id ?? null);
|
|
1444
|
+
pushNetworkObserverEvent(effectiveNetId ?? task.network_id ?? null, { type: "new_task", task_id, from: from_session, to: reassignedAlias, status: "delivered", priority: task.priority });
|
|
1422
1445
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: reassignedAlias, ...(canonical.renamed ? { renamed_from: new_alias, renamed_to: reassignedAlias } : {}) }) }] };
|
|
1423
1446
|
}
|
|
1424
1447
|
);
|
package/src/uploads-http.test.ts
CHANGED
|
@@ -14,8 +14,11 @@ import { register, login } from "./auth.js";
|
|
|
14
14
|
|
|
15
15
|
const SERVER_DB = mkdtempSync(join(tmpdir(), "anet-upload-http-db-")) + "/commhub.db";
|
|
16
16
|
const UPLOADS_DIR = mkdtempSync(join(tmpdir(), "anet-upload-http-fs-"));
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// #438 corrective: private bootServer({ port: 0 }) instance — the OS
|
|
18
|
+
// assigns the port and BASE is derived from the ACTUAL bound port, so
|
|
19
|
+
// this suite is immune to both the parent-side random-port TOCTOU and
|
|
20
|
+
// the aggregate module-cache no-op (import no longer boots anything).
|
|
21
|
+
let BASE = "";
|
|
19
22
|
|
|
20
23
|
let server: any;
|
|
21
24
|
let userToken = "";
|
|
@@ -27,7 +30,6 @@ beforeAll(async () => {
|
|
|
27
30
|
// we want to validate the production code path).
|
|
28
31
|
process.env.COMMHUB_DB = SERVER_DB;
|
|
29
32
|
process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
|
|
30
|
-
process.env.PORT = String(PORT);
|
|
31
33
|
process.env.HOST = "127.0.0.1";
|
|
32
34
|
|
|
33
35
|
// Use a unique username + strong password each run so we don't trip
|
|
@@ -50,16 +52,17 @@ beforeAll(async () => {
|
|
|
50
52
|
}
|
|
51
53
|
expect(userToken).toBeTruthy();
|
|
52
54
|
|
|
53
|
-
//
|
|
54
|
-
// the port
|
|
55
|
-
await import("./
|
|
55
|
+
// Importing is side-effect-free now; boot a private instance and
|
|
56
|
+
// read the real port back.
|
|
57
|
+
const { bootServer } = await import("./server.js");
|
|
58
|
+
server = bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
59
|
+
BASE = `http://127.0.0.1:${server.port}`;
|
|
56
60
|
// Tiny settle so the listener is ready.
|
|
57
61
|
await new Promise((r) => setTimeout(r, 100));
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
afterAll(() => {
|
|
61
|
-
|
|
62
|
-
// time so we can't easily stop it — process exit handles that.
|
|
65
|
+
try { server?.stop?.(true); } catch {}
|
|
63
66
|
try { rmSync(UPLOADS_DIR, { recursive: true, force: true }); } catch {}
|
|
64
67
|
try { rmSync(SERVER_DB, { recursive: true, force: true }); } catch {}
|
|
65
68
|
});
|