@sleep2agi/commhub-server 0.9.0-preview.51 → 0.9.0-preview.52
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.
|
|
3
|
+
"version": "0.9.0-preview.52",
|
|
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,92 @@
|
|
|
1
|
+
// 桌面向导要看得到 daemon 回的失败原因:GET /api/node-create-requests?request_id=…
|
|
2
|
+
// 承重:① 只在调用者的网络作用域里能读到(别的网络 → 404,与不存在同形);② 无用户上下文 401;③ 缺参 400;
|
|
3
|
+
// ④ 回的是 daemon ack 写进去的 status/error 原文。
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
5
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const PRIVATE_DB_DIR = mkdtempSync(join(tmpdir(), "anet-create-request-status-"));
|
|
10
|
+
process.env.COMMHUB_DB ||= join(PRIVATE_DB_DIR, "hub.db");
|
|
11
|
+
|
|
12
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
13
|
+
let base = "";
|
|
14
|
+
let aToken = "", bToken = "", aNet = "", bNet = "";
|
|
15
|
+
const MASTER_TOKEN = "legacy-master-token-for-test";
|
|
16
|
+
|
|
17
|
+
const get = async (path: string, token: string) => {
|
|
18
|
+
const r = await fetch(`${base}${path}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
19
|
+
return { status: r.status, body: await r.json() as any };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
process.env.COMMHUB_AUTH_TOKEN ||= MASTER_TOKEN;
|
|
24
|
+
const { db } = await import("./db.js");
|
|
25
|
+
const { register } = await import("./auth.js");
|
|
26
|
+
const stamp = Date.now();
|
|
27
|
+
const admin = register(`cr_admin_${stamp}`, "CrAdmin-Strong-1!");
|
|
28
|
+
expect(admin.ok).toBe(true);
|
|
29
|
+
const a = register(`cr_a_${stamp}`, "CrA-Strong-1!");
|
|
30
|
+
const b = register(`cr_b_${stamp}`, "CrB-Strong-1!");
|
|
31
|
+
expect(a.ok && b.ok).toBe(true);
|
|
32
|
+
aToken = a.token!; bToken = b.token!; aNet = a.network_id!; bNet = b.network_id!;
|
|
33
|
+
const uid = (name: string) => db.get<{ user_id: string }>("SELECT user_id FROM users WHERE username = ?1", name)!.user_id;
|
|
34
|
+
for (const id of [uid(`cr_a_${stamp}`), uid(`cr_b_${stamp}`)]) {
|
|
35
|
+
expect(db.get<{ role: string }>("SELECT role FROM users WHERE user_id = ?1", id)?.role).toBe("user");
|
|
36
|
+
}
|
|
37
|
+
db.run(
|
|
38
|
+
`INSERT INTO node_create_requests
|
|
39
|
+
(request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, child_token_id, created_at, created_by_token, acked_at, error)
|
|
40
|
+
VALUES ('cr_a_failed', 'node_daemon_a', 'vansin-go', ?1, 'opencode-cli', NULL, '{}', '[]', 'failed', NULL, ?2, 'tok', ?2,
|
|
41
|
+
'opencode-cli package entrypoint verification currently requires Linux')`,
|
|
42
|
+
[aNet, Date.now()],
|
|
43
|
+
);
|
|
44
|
+
db.run(
|
|
45
|
+
`INSERT INTO node_create_requests
|
|
46
|
+
(request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, child_token_id, created_at, created_by_token)
|
|
47
|
+
VALUES ('cr_b_pending', 'node_daemon_b', 'other', ?1, 'codex-app-server', NULL, '{}', '[]', 'pending', NULL, ?2, 'tok')`,
|
|
48
|
+
[bNet, Date.now()],
|
|
49
|
+
);
|
|
50
|
+
const mod = await import("./server.js");
|
|
51
|
+
server = mod.bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
52
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterAll(() => {
|
|
56
|
+
try { server?.stop(true); } catch {}
|
|
57
|
+
try { rmSync(PRIVATE_DB_DIR, { recursive: true, force: true }); } catch {}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("GET /api/node-create-requests?request_id=", () => {
|
|
61
|
+
test("A reads her own failed request with the daemon's error verbatim", async () => {
|
|
62
|
+
const { status, body } = await get("/api/node-create-requests?request_id=cr_a_failed", aToken);
|
|
63
|
+
expect(status).toBe(200);
|
|
64
|
+
expect(body.ok).toBe(true);
|
|
65
|
+
expect(body.request).toMatchObject({
|
|
66
|
+
request_id: "cr_a_failed", child_name: "vansin-go", runtime: "opencode-cli", status: "failed",
|
|
67
|
+
error: "opencode-cli package entrypoint verification currently requires Linux",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
test("🔴 B cannot read A's request — 404, same shape as nonexistent", async () => {
|
|
71
|
+
const other = await get("/api/node-create-requests?request_id=cr_a_failed", bToken);
|
|
72
|
+
const missing = await get("/api/node-create-requests?request_id=cr_nope", bToken);
|
|
73
|
+
expect(other.status).toBe(404);
|
|
74
|
+
expect(missing.status).toBe(404);
|
|
75
|
+
expect(other.body).toEqual(missing.body);
|
|
76
|
+
});
|
|
77
|
+
test("pending request reads as pending with no error", async () => {
|
|
78
|
+
const { body } = await get("/api/node-create-requests?request_id=cr_b_pending", bToken);
|
|
79
|
+
expect(body.request.status).toBe("pending");
|
|
80
|
+
expect(body.request.error).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
test("missing request_id → 400", async () => {
|
|
83
|
+
const { status, body } = await get("/api/node-create-requests", aToken);
|
|
84
|
+
expect(status).toBe(400);
|
|
85
|
+
expect(body.error).toBe("request_id_required");
|
|
86
|
+
});
|
|
87
|
+
test("legacy master token (no user context) → 401", async () => {
|
|
88
|
+
const { status, body } = await get("/api/node-create-requests?request_id=cr_a_failed", MASTER_TOKEN);
|
|
89
|
+
expect(status).toBe(401);
|
|
90
|
+
expect(body.error).toBe("auth_required");
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/server.ts
CHANGED
|
@@ -3120,6 +3120,31 @@ return Bun.serve({
|
|
|
3120
3120
|
// Mirror of the `list_host_supervisors` MCP tool for non-MCP callers.
|
|
3121
3121
|
// Same SQL + role-extract + member-脱敏 logic; SEC-1 scoped via REST
|
|
3122
3122
|
// auth pipeline (restScope). Returns {ok, daemons:[...], count}.
|
|
3123
|
+
// ── REST: one node_create_request's outcome (app#… 桌面向导) ──
|
|
3124
|
+
// 向导 POST create_node 后只会轮询「子节点注册了没」,daemon 回的失败原因(ack_create_request 写进
|
|
3125
|
+
// node_create_requests.status/error)此前没有任何读接口 —— 用户只看到「24s 没注册」。
|
|
3126
|
+
// 这里按 request_id 读一行;网络作用域与其它 REST 相同(addNetworkScope),无用户上下文 401。
|
|
3127
|
+
if (url.pathname === "/api/node-create-requests" && req.method === "GET") {
|
|
3128
|
+
if (!restAuth) {
|
|
3129
|
+
return withCors(req, Response.json({ ok: false, error: "auth_required" }, { status: 401 }));
|
|
3130
|
+
}
|
|
3131
|
+
const requestId = url.searchParams.get("request_id")?.trim() ?? "";
|
|
3132
|
+
if (!requestId) {
|
|
3133
|
+
return withCors(req, Response.json({ ok: false, error: "request_id_required" }, { status: 400 }));
|
|
3134
|
+
}
|
|
3135
|
+
const params: any[] = [requestId];
|
|
3136
|
+
let sql = `SELECT request_id, daemon_node_id, child_name, network_id, runtime, model, status, error,
|
|
3137
|
+
created_at, delivered_at, acked_at
|
|
3138
|
+
FROM node_create_requests WHERE request_id = ?1`;
|
|
3139
|
+
sql = addNetworkScope(sql, params, restScope);
|
|
3140
|
+
const row = db.get<Record<string, unknown>>(sql, ...params);
|
|
3141
|
+
if (!row) {
|
|
3142
|
+
// 不存在与不在你作用域内长一样:不泄漏别的网络里有没有这个 id。
|
|
3143
|
+
return withCors(req, Response.json({ ok: false, error: "request_not_found" }, { status: 404 }));
|
|
3144
|
+
}
|
|
3145
|
+
return withCors(req, Response.json({ ok: true, request: row }));
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3123
3148
|
if (url.pathname === "/api/host-supervisors") {
|
|
3124
3149
|
// #380 — utok callers with exactly one accessible network get a
|
|
3125
3150
|
// safe fallback so the create-node wizard doesn't have to bake
|