🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@sleep2agi/commhub-server

Package Overview
Dependencies
Maintainers
1
Versions
91
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sleep2agi/commhub-server - npm Package Compare versions

Comparing version
0.9.0-preview.6
to
0.9.0-preview.7
+314
src/list-host-supervisors.test.ts
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
import { db, hashToken, generateId } from "./db.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerTools } from "./tools.js";
// RFC-026 §9.2 / #338 PR2 v2 — list_host_supervisors HANDLER tests.
//
// PR2 v1 used a mirror-SQL pattern (re-implemented the query in the test
// itself) → trivially passed but didn't exercise the handler. 通信龙 audit
// caught: that style misses the SEC-1 scope-derivation bug
// (`getNetworkId(clientNetId)` returns the unchecked client-supplied
// network_id for utok_ callers when enforceNetworkId is null, leaking
// cross-tenant daemons). This rewrite drives the registered MCP tool
// through registerTools() so the auth context is real.
//
// Covers:
// SEC-1.1 — cross-tenant utok_ caller with foreign network_id: DENIED
// SEC-1.2 — admin caller with own network_id: returns own daemons
// SEC-1.3 — caller with no network_id: returns daemons in all member networks
// masking — non-admin viewer: host_telemetry IP/cpu/mem stripped, alert_level still surfaces
// revoked — DELETEd token row (revokeToken path): daemon excluded
// revoked — revoked_at SET token: daemon excluded
// self-declare — runtimes_supported/allowed_secret_keys surface from columns
// pre-PR2 daemon — empty arrays on NULL columns (back-compat)
const NET_ALPHA = "net_lhs_alpha";
const NET_BETA = "net_lhs_beta";
interface ToolHandler { (args: any, extra?: any): Promise<{ content: Array<{ type: "text"; text: string }> }>; }
interface Reply { ok?: boolean; error?: string; daemons?: any[]; count?: number; }
function cleanup() {
for (const n of [NET_ALPHA, NET_BETA]) {
try { db.run("DELETE FROM nodes WHERE network_id = ?1", [n]); } catch {}
try { db.run("DELETE FROM sessions WHERE network_id = ?1", [n]); } catch {}
try { db.run("DELETE FROM api_tokens WHERE network_id = ?1", [n]); } catch {}
try { db.run("DELETE FROM network_members WHERE network_id = ?1", [n]); } catch {}
try { db.run("DELETE FROM networks WHERE network_id = ?1", [n]); } catch {}
}
try { db.run("DELETE FROM users WHERE user_id LIKE 'u_lhs_%'"); } catch {}
}
beforeEach(cleanup);
afterAll(cleanup);
function seedUser(user_id: string) {
db.run(
`INSERT INTO users (user_id, username, password_hash, role, created_at)
VALUES (?1, ?2, 'x', 'user', datetime('now'))`,
[user_id, user_id],
);
}
function seedNetwork(net_id: string, owner_id = "u_lhs_owner") {
db.run(
`INSERT OR REPLACE INTO networks (network_id, network_name, owner_id, created_at)
VALUES (?1, ?2, ?3, datetime('now'))`,
[net_id, net_id, owner_id],
);
}
function seedMembership(user_id: string, net_id: string, role: "owner" | "admin" | "member" | "viewer" = "member") {
db.run(
`INSERT INTO network_members (user_id, network_id, role, joined_at)
VALUES (?1, ?2, ?3, datetime('now'))`,
[user_id, net_id, role],
);
}
function seedDaemon(opts: {
network_id: string, alias: string, node_id?: string,
role?: string,
runtimes?: string[],
secrets?: string[],
sessionAgeMs?: number,
mintToken?: boolean,
tokenRevokedAt?: boolean,
tokenDeleted?: boolean,
}) {
const node_id = opts.node_id || `node_${opts.network_id}_${opts.alias}`;
const snapshot = JSON.stringify({
model: "claude-opus", flags: {}, role: opts.role ?? "host_supervisor",
runtimes_supported: opts.runtimes ?? ["claude-agent-sdk"],
allowed_secret_keys: opts.secrets ?? [],
});
db.run(
`INSERT OR REPLACE INTO nodes (
node_id, node_name, alias, network_id,
runtimes_supported, allowed_secret_keys,
config_snapshot, hostname,
created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, datetime('now'), datetime('now'))`,
[node_id, opts.alias, opts.alias, opts.network_id,
JSON.stringify(opts.runtimes ?? ["claude-agent-sdk"]),
JSON.stringify(opts.secrets ?? []),
snapshot, `host-${opts.alias}`],
);
if (opts.sessionAgeMs !== undefined) {
const ts = new Date(Date.now() - opts.sessionAgeMs).toISOString().replace("T", " ").replace(/\..+/, "");
db.run(
`INSERT OR REPLACE INTO sessions (resume_id, alias, network_id, last_seen_at, status, cpu_cores, mem_total_gb, ip)
VALUES (?1, ?2, ?3, ?4, 'idle', 8, 16, '10.0.0.5')`,
[`s_${node_id}`, opts.alias, opts.network_id, ts],
);
}
if (opts.mintToken !== false && !opts.tokenDeleted) {
db.run(
`INSERT OR REPLACE INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, ?6)`,
[`tok_${node_id}`, `u_seed_${node_id}`, opts.network_id, `node:${opts.alias}`,
`hash_${node_id}`,
opts.tokenRevokedAt ? new Date().toISOString().replace("T", " ").replace(/\..+/, "") : null],
);
}
return node_id;
}
/** Build a server with registerTools wired to a specific enforceUserId. */
function buildToolHandler(enforceUserId: string | null): ToolHandler {
const server = new McpServer({ name: "test", version: "0" }) as any;
const tools: Record<string, ToolHandler> = {};
// Capture the registered tool function. The MCP SDK's server.tool stores
// the handler internally; we intercept by overriding before registerTools.
const origTool = server.tool.bind(server);
server.tool = (name: string, _desc: string, _schema: any, handler: ToolHandler) => {
tools[name] = handler;
return origTool(name, _desc, _schema, handler);
};
// Some tools use registerTool; intercept that too
const origRegisterTool = server.registerTool?.bind(server);
if (origRegisterTool) {
server.registerTool = (name: string, _cfg: any, handler: ToolHandler) => {
tools[name] = handler;
return origRegisterTool(name, _cfg, handler);
};
}
registerTools(
server,
/* clientIP */ undefined,
/* enforceNetworkId */ null,
/* enforceUserId */ enforceUserId,
/* callerAlias */ null,
/* callerTokenIsNetwork */ false,
/* callerTokenId */ null,
);
const h = tools["list_host_supervisors"];
if (!h) throw new Error("list_host_supervisors tool not registered");
return h;
}
async function callList(handler: ToolHandler, args: any = {}): Promise<Reply> {
const r = await handler(args);
return JSON.parse(r.content[0].text) as Reply;
}
describe("list_host_supervisors HANDLER tests (RFC-026 §9.2, PR2 v2)", () => {
test("SEC-1.1: cross-tenant utok_ caller with foreign network_id → DENIED", async () => {
const userA = "u_lhs_alice"; // belongs to NET_ALPHA only
seedUser(userA);
seedNetwork(NET_ALPHA);
seedNetwork(NET_BETA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_BETA, alias: "secret-beta-daemon" });
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_BETA });
// PR2 v1 BUG: this returned ok:true with secret-beta-daemon in the list.
expect(reply.ok).toBe(false);
expect(reply.error).toMatch(/access denied|not a member/i);
// Belt: hostname not in error payload
expect(JSON.stringify(reply)).not.toContain("secret-beta-daemon");
expect(JSON.stringify(reply)).not.toContain("host-secret-beta-daemon");
});
test("SEC-1.2: admin caller with own network_id returns own daemons", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_ALPHA, alias: "alpha-daemon-1" });
seedDaemon({ network_id: NET_ALPHA, alias: "alpha-daemon-2" });
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
expect(reply.ok).toBe(true);
const aliases = (reply.daemons ?? []).map(d => d.alias).sort();
expect(aliases).toEqual(["alpha-daemon-1", "alpha-daemon-2"]);
});
test("SEC-1.3: caller without network_id arg → returns daemons across member networks only", async () => {
const userA = "u_lhs_alice"; // member of NET_ALPHA only
seedUser(userA);
seedNetwork(NET_ALPHA);
seedNetwork(NET_BETA);
seedMembership(userA, NET_ALPHA, "member");
seedDaemon({ network_id: NET_ALPHA, alias: "ok-daemon" });
seedDaemon({ network_id: NET_BETA, alias: "forbidden-daemon" });
const handler = buildToolHandler(userA);
const reply = await callList(handler);
expect(reply.ok).toBe(true);
const aliases = (reply.daemons ?? []).map(d => d.alias);
expect(aliases).toContain("ok-daemon");
expect(aliases).not.toContain("forbidden-daemon");
});
test("masking: member caller gets host_telemetry without IP/cpu/mem; admin gets full", async () => {
const userMember = "u_lhs_bob_member";
const userAdmin = "u_lhs_carol_admin";
seedUser(userMember);
seedUser(userAdmin);
seedNetwork(NET_ALPHA);
seedMembership(userMember, NET_ALPHA, "member");
seedMembership(userAdmin, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_ALPHA, alias: "telemetry-daemon", sessionAgeMs: 5_000 });
const memberHandler = buildToolHandler(userMember);
const memberReply = await callList(memberHandler, { network_id: NET_ALPHA });
expect(memberReply.daemons?.[0].host_telemetry.alert_level).toBeDefined();
expect(memberReply.daemons?.[0].host_telemetry).not.toHaveProperty("cpu_cores");
expect(memberReply.daemons?.[0].host_telemetry).not.toHaveProperty("mem_gb");
expect(memberReply.daemons?.[0].host_telemetry).not.toHaveProperty("ip_internal");
const adminHandler = buildToolHandler(userAdmin);
const adminReply = await callList(adminHandler, { network_id: NET_ALPHA });
expect(adminReply.daemons?.[0].host_telemetry.cpu_cores).toBe(8);
expect(adminReply.daemons?.[0].host_telemetry.mem_gb).toBe(16);
expect(adminReply.daemons?.[0].host_telemetry.ip_internal).toBe("10.0.0.5");
});
test("revoked: DELETEd token row (revokeToken path) excludes daemon", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_ALPHA, alias: "alive-daemon" });
seedDaemon({ network_id: NET_ALPHA, alias: "deleted-token-daemon", tokenDeleted: true });
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
const aliases = (reply.daemons ?? []).map(d => d.alias).sort();
expect(aliases).toContain("alive-daemon");
expect(aliases).not.toContain("deleted-token-daemon");
});
test("revoked: token with revoked_at SET also excludes daemon", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_ALPHA, alias: "alive-daemon" });
seedDaemon({ network_id: NET_ALPHA, alias: "revoked-at-daemon", tokenRevokedAt: true });
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
const aliases = (reply.daemons ?? []).map(d => d.alias).sort();
expect(aliases).toContain("alive-daemon");
expect(aliases).not.toContain("revoked-at-daemon");
});
test("role filter: regular member nodes (role=member) excluded after extraction", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({ network_id: NET_ALPHA, alias: "real-daemon", role: "host_supervisor" });
seedDaemon({ network_id: NET_ALPHA, alias: "regular-node", role: "member" });
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
const aliases = (reply.daemons ?? []).map(d => d.alias).sort();
expect(aliases).toEqual(["real-daemon"]);
});
test("self-declare: runtimes_supported + allowed_secret_keys surface from columns", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
seedDaemon({
network_id: NET_ALPHA, alias: "rich-daemon",
runtimes: ["claude-agent-sdk", "codex-sdk", "grok-build-acp"],
secrets: ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
});
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
const d = reply.daemons?.[0];
expect(d.runtimes_supported).toEqual(["claude-agent-sdk", "codex-sdk", "grok-build-acp"]);
expect(d.allowed_secret_keys).toEqual(["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]);
});
test("pre-PR2 daemon (NULL columns) extracts to empty arrays back-compat", async () => {
const userA = "u_lhs_alice";
seedUser(userA);
seedNetwork(NET_ALPHA);
seedMembership(userA, NET_ALPHA, "admin");
db.run(
`INSERT INTO nodes (node_id, node_name, alias, network_id, config_snapshot, runtimes_supported, allowed_secret_keys, created_at, updated_at)
VALUES (?1, 'legacy', 'legacy', ?2, ?3, NULL, NULL, datetime('now'), datetime('now'))`,
["node_legacy_alpha", NET_ALPHA, JSON.stringify({ role: "host_supervisor" })],
);
// Seed active token so the EXISTS guard lets it through
db.run(
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash)
VALUES ('tok_legacy', 'u_seed_legacy', ?1, 'network', 'node:legacy', 'hash_legacy')`,
[NET_ALPHA],
);
const handler = buildToolHandler(userA);
const reply = await callList(handler, { network_id: NET_ALPHA });
const d = reply.daemons?.find(d => d.alias === "legacy");
expect(d).toBeTruthy();
expect(d.runtimes_supported).toEqual([]);
expect(d.allowed_secret_keys).toEqual([]);
});
});
+1
-1
{
"name": "@sleep2agi/commhub-server",
"version": "0.9.0-preview.6",
"version": "0.9.0-preview.7",
"description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -304,2 +304,25 @@ import { createAdapter, type DbAdapter } from "./db-adapter";

// RFC-026 §9 / #338 PR2 — daemon self-declare fields promoted from
// config_snapshot to first-class indexable columns. The hub
// `list_host_supervisors` MCP tool reads these directly without parsing
// the snapshot JSON on every list call.
//
// Both fields are JSON arrays of strings:
// runtimes_supported e.g. ["claude-agent-sdk","codex-sdk","grok-build-acp"]
// allowed_secret_keys e.g. ["ANTHROPIC_API_KEY","OPENAI_API_KEY"]
//
// Stored as TEXT (JSON-encoded) — SQLite has no native array type and
// the wrapper code is small. Both nullable for pre-PR2 daemons; the
// list endpoint falls back to [] when null.
try {
db.exec(`ALTER TABLE nodes ADD COLUMN runtimes_supported TEXT`);
} catch (e: any) {
if (!/duplicate column|already exists/i.test(e?.message || "")) throw e;
}
try {
db.exec(`ALTER TABLE nodes ADD COLUMN allowed_secret_keys TEXT`);
} catch (e: any) {
if (!/duplicate column|already exists/i.test(e?.message || "")) throw e;
}
// `node_config_updates` — pending + history. One row per dashboard write.

@@ -306,0 +329,0 @@ // At most one row per node may be in a non-terminal state at a time

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display