@sleep2agi/commhub-server 0.9.0-preview.41 → 0.9.0-preview.42

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.9.0-preview.41",
3
+ "version": "0.9.0-preview.42",
4
4
  "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,76 @@
1
+ // report_status 的 host 遥测 schema 必须接受 agent-node **声明会发**的 null。
2
+ //
3
+ // 来源不是推理,是 #1225 复现容器里量到的一次真崩:`--network none` 里
4
+ // agent-node 一启动就死在
5
+ // MCP error -32602: Invalid input: expected string, received null at host.ip
6
+ // 而 register() 是 `await callCommHub("report_status", …)` 且没有 catch ——
7
+ // 于是**整个节点进程当场退出**,和 runtime 无关(opencode/claude/codex 一样)。
8
+ //
9
+ // 触发条件不是"容器",是"这台机器没有非回环 IPv4":
10
+ // agent-node 的 firstNonInternalIPv4()(host-telemetry.ts:48-59)在那种机器上
11
+ // 返回 null,而 HostTelemetry 的类型本来就写着 `ip: string | null`。
12
+ //
13
+ // 🔴 这里断言的是 **schema 本身**,不是 handler。直接调 handler 会绕过 zod ——
14
+ // 而缺陷恰恰只存在于 zod 那一层,绕过去就永远绿。
15
+ import { describe, expect, test } from "bun:test";
16
+ import { z } from "zod";
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { registerTools } from "./tools.js";
19
+
20
+ function captureToolSchemas(): Record<string, any> {
21
+ const mcp = new McpServer({ name: "schema-probe", version: "0" }) as any;
22
+ const schemas: Record<string, any> = {};
23
+ const origTool = mcp.tool.bind(mcp);
24
+ mcp.tool = (name: string, desc: string, schema: any, handler: any) => {
25
+ schemas[name] = schema;
26
+ return origTool(name, desc, schema, handler);
27
+ };
28
+ registerTools(mcp, undefined, null, null, null, false, "tok_schema_probe");
29
+ return schemas;
30
+ }
31
+
32
+ const schemas = captureToolSchemas();
33
+
34
+ describe("report_status host telemetry schema", () => {
35
+ test("前提:抓到的确实是 report_status 的 host 形状", () => {
36
+ expect(schemas.report_status).toBeTruthy();
37
+ expect(schemas.report_status.host).toBeTruthy();
38
+ });
39
+
40
+ test("🔴 host.ip = null 必须被接受(没有非回环 IPv4 的机器就是发 null)", () => {
41
+ const host = schemas.report_status.host;
42
+ const parsed = host.parse({ hostname: "boxy", ip: null });
43
+ expect(parsed.ip).toBeNull();
44
+ });
45
+
46
+ test("🔴 host.hostname = null 同样被接受(与 ip 同为字符串字段,一起对齐)", () => {
47
+ const host = schemas.report_status.host;
48
+ expect(host.parse({ hostname: null, ip: "10.0.0.2" }).hostname).toBeNull();
49
+ });
50
+
51
+ test("agent-node 那条完整心跳负载整体过 schema", () => {
52
+ // 形状抄自 agent-node/src/host-telemetry.ts 的 HostTelemetry:
53
+ // 没有网络、没有 /proc 时每一项都可能是 null。
54
+ const full = {
55
+ hostname: "unknown", ip: null,
56
+ cpu_load_1min: null, cpu_cores: null,
57
+ mem_total_gb: null, mem_used_gb: null, mem_avail_gb: null,
58
+ disk_total_gb: null, disk_used_gb: null, disk_avail_gb: null,
59
+ };
60
+ expect(() => schemas.report_status.host.parse(full)).not.toThrow();
61
+ });
62
+
63
+ test("放宽的只有 null —— 类型错的值仍然被拒", () => {
64
+ const host = schemas.report_status.host;
65
+ expect(() => host.parse({ ip: 12345 })).toThrow();
66
+ expect(() => host.parse({ ip: "x".repeat(201) })).toThrow();
67
+ });
68
+
69
+ test("整条 report_status 负载(不只是 host 子对象)也接受 ip=null", () => {
70
+ const full = z.object(schemas.report_status);
71
+ expect(() => full.parse({
72
+ resume_id: "probe-resume", alias: "probe-node", status: "idle",
73
+ host: { hostname: "unknown", ip: null },
74
+ })).not.toThrow();
75
+ });
76
+ });
package/src/tools.ts CHANGED
@@ -518,8 +518,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
518
518
  node_name: z.string().max(200).optional().describe("Stable node display name (may differ from alias)"),
519
519
  network_id: z.string().max(200).optional().describe("Network this agent belongs to"),
520
520
  host: z.object({
521
- hostname: z.string().max(200).optional(),
522
- ip: z.string().max(200).optional(),
521
+ // 🔴 `.nullable()` 不是防御性冗余,是**发送方声明的类型**:agent-node 的
522
+ // HostTelemetry 是 `ip: string | null`,`firstNonInternalIPv4()` 在没有
523
+ // 非回环 IPv4 的机器上返回 null(--network none 的容器、断网的笔记本、
524
+ // 部分 CI runner)。此前这里只有 `.optional()`,于是那台机器上的
525
+ // agent-node 一启动就收到 MCP -32602,而 register() 是 await 且无人 catch
526
+ // —— **整个节点进程当场死掉**,和 runtime 无关。
527
+ // 同一个对象里四个数值字段本来就写着 `.nullable().optional()`,
528
+ // 两个字符串字段是唯一的例外;正确写法一直在隔壁那一行。
529
+ hostname: z.string().max(200).nullable().optional(),
530
+ ip: z.string().max(200).nullable().optional(),
523
531
  cpu_load_1min: z.number().nullable().optional(),
524
532
  cpu_cores: z.number().nullable().optional(),
525
533
  mem_total_gb: z.number().nullable().optional(),