@sleep2agi/commhub-server 0.9.0-preview.92 → 0.9.0-preview.94
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/auth.ts +3 -3
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/departments.ts +31 -10
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/human-dm.ts +2 -2
- package/src/mcp-context-cost-http.test.ts +4 -2
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-permissions-http.test.ts +18 -0
- package/src/requirement-due-reminders-http.test.ts +390 -0
- package/src/requirement-due-reminders.ts +310 -0
- package/src/requirements-errors.ts +2 -0
- package/src/requirements-status-participants-http.test.ts +5 -1
- package/src/requirements.ts +16 -1
- package/src/server.ts +171 -10
- package/src/tool-audience-http.test.ts +195 -0
- package/src/tool-audience.ts +109 -0
- package/src/tools-list-json-schema.test.ts +2 -1
- package/src/tools.ts +36 -6
|
@@ -19,7 +19,8 @@ import { registerTools } from "./tools.js";
|
|
|
19
19
|
|
|
20
20
|
async function connect() {
|
|
21
21
|
const server = new McpServer({ name: "list-tools-test", version: "1" });
|
|
22
|
-
|
|
22
|
+
// 没有用户 / 节点身份的调用者 = tools/list 不按角色过滤(#478),这里要的正是「每个登记的工具」。
|
|
23
|
+
registerTools(server, undefined, "net_list_tools", null, "u_list_tools", false, "tok_list_tools");
|
|
23
24
|
const client = new Client({ name: "list-tools-client", version: "1" });
|
|
24
25
|
const [ct, st] = InMemoryTransport.createLinkedPair();
|
|
25
26
|
await server.connect(st);
|
package/src/tools.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { pendingInboxCount } from "./inbox-count.js";
|
|
|
13
13
|
import { getUserNetworkRole, createNetworkTokenForNode } from "./auth.js";
|
|
14
14
|
import { addAgentNetworkScope, addNetworkScope, addOwnTrafficScope, canRestWriteNetwork, canRestWriteNetworkAsHuman, getUserNetworkIds, resolveRestNetworkScope, singleNetworkId, type RestNetworkScope } from "./network-scope.js";
|
|
15
15
|
import { canMessageAgent, restrictedNetworkIds, RESTRICTED_MEMBER_TOOLS, type AgentRef } from "./agent-access.js";
|
|
16
|
+
import { listedToolFilter, scopeToolsList, type ToolCaller } from "./tool-audience.js";
|
|
16
17
|
import { broadcastVerdict, dispatchVerdict, humanOnlyVerdict, NODE_TOOL_CLASS, nodeDecide, nodeIdentity, nodePermissionDeniedBody, writeVerdict, type NodeIdentity, type Verdict } from "./node-permissions.js";
|
|
17
18
|
import { restrictedMemberAttachmentsDenied } from "./restricted-files.js";
|
|
18
19
|
import { errorBody } from "./requirements-errors.js";
|
|
@@ -258,7 +259,7 @@ function guardNodePermissionTools(server: McpServer, tokenId: string, networkId:
|
|
|
258
259
|
}
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false, callerTokenId?: string | null) {
|
|
262
|
+
export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null, enforceUserId?: string | null, callerAlias?: string | null, callerTokenIsNetwork = false, callerTokenId?: string | null, listing: { includeProtocol?: boolean } = {}) {
|
|
262
263
|
// 多用户 Agent 权限:用户令牌调用者在哪些网络里是受限成员(只看授权 Agent)。
|
|
263
264
|
// 网络令牌不会走到这里 —— 受限成员的网络令牌在 resolveToken 就被拒了。
|
|
264
265
|
const restrictedNets = enforceUserId && !callerTokenIsNetwork && !enforceNetworkId ? restrictedNetworkIds(enforceUserId) : [];
|
|
@@ -1166,7 +1167,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1166
1167
|
if (node_id) {
|
|
1167
1168
|
try {
|
|
1168
1169
|
const nodeRuntime = ag?.includes(":") ? ag.split(":")[1] + "-sdk" : ag ?? null;
|
|
1169
|
-
upsertNodeWithSec1Guard({
|
|
1170
|
+
const upserted = upsertNodeWithSec1Guard({
|
|
1170
1171
|
node_id,
|
|
1171
1172
|
callerNetworkId: effectiveNetId ?? null,
|
|
1172
1173
|
callerUserId: enforceUserId ?? null,
|
|
@@ -1181,6 +1182,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
1181
1182
|
hostname: hn ?? null,
|
|
1182
1183
|
config_snapshot: cfgSnap ?? null,
|
|
1183
1184
|
});
|
|
1185
|
+
// The session row above already took node_id; a refused identity claim
|
|
1186
|
+
// must not leave this session pointing at another node's row.
|
|
1187
|
+
if (upserted.result === "refused" && upserted.reason === "identity_mismatch") {
|
|
1188
|
+
db.run("UPDATE sessions SET node_id = NULL WHERE resume_id = ?1 AND node_id = ?2", [resume_id, node_id]);
|
|
1189
|
+
}
|
|
1184
1190
|
} catch {}
|
|
1185
1191
|
}
|
|
1186
1192
|
|
|
@@ -5957,6 +5963,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5957
5963
|
parent_id: z.string().max(200).optional().describe("children of this requirement ('none' = top level)"),
|
|
5958
5964
|
department_id: z.string().max(64).optional().describe("owner (or its Agent's owner) in this department or below"),
|
|
5959
5965
|
top_level: z.boolean().optional(), q: z.string().max(200).optional(),
|
|
5966
|
+
overdue: z.boolean().optional().describe("true = open tasks past their due (date-only due: before today, Asia/Shanghai); false = everything else"),
|
|
5967
|
+
due_within_days: z.number().int().min(0).max(365).optional().describe("open tasks not yet overdue, due by the end of today + N days (0 = later today)"),
|
|
5960
5968
|
limit: z.number().int().min(1).max(1000).optional().describe(`page size, default ${REQ_LIST_MCP_DEFAULT_LIMIT}`),
|
|
5961
5969
|
cursor: z.string().max(600).optional().describe("next_cursor from the previous page"),
|
|
5962
5970
|
view: z.enum(["full", "summary"]).optional().describe("default summary (no description / checklist bodies; has_description, checklist_count instead); full = everything"),
|
|
@@ -5966,7 +5974,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5966
5974
|
server.registerTool(
|
|
5967
5975
|
"requirements_list",
|
|
5968
5976
|
{
|
|
5969
|
-
description: `List requirement tasks in your network, newest first. Defaults (MCP): view='summary' (no description / checklist bodies; has_description, checklist_count instead; ~0.5 KB per task) and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page; has_more + next_cursor → pass cursor. Full text: requirements_get, or view='full' with a small limit (≤ 1000). Filters: seq (#N), status, project_id ('none'), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact), updated_since (ISO), external_ref, parent_id / top_level, department_id (owner in it or below), include_archived; q searches title / description / people / project / tags (terms ANDed). Rows carry children {total, done}. changes=true (with updated_since) returns only tasks changed since then, archived included, plus deleted ids and server_time for the next updated_since. Unknown parameters are rejected (-32602).`,
|
|
5977
|
+
description: `List requirement tasks in your network, newest first. Defaults (MCP): view='summary' (no description / checklist bodies; has_description, checklist_count instead; ~0.5 KB per task) and ${REQ_LIST_MCP_DEFAULT_LIMIT} per page; has_more + next_cursor → pass cursor. Full text: requirements_get, or view='full' with a small limit (≤ 1000). Filters: seq (#N), status, project_id ('none'), owner / agent_owner ('user:<id>' / 'node:<id>' / 'none'), tag (exact), updated_since (ISO), overdue / due_within_days (open tasks only), external_ref, parent_id / top_level, department_id (owner in it or below), include_archived; q searches title / description / people / project / tags (terms ANDed). Rows carry children {total, done}. changes=true (with updated_since) returns only tasks changed since then, archived included, plus deleted ids and server_time for the next updated_since. Unknown parameters are rejected (-32602).`,
|
|
5970
5978
|
// Strict: registerTool honours a constructed object's unknownKeys (server.tool() would strip them silently).
|
|
5971
5979
|
inputSchema: z.strictObject(reqListShape, {
|
|
5972
5980
|
error: (iss: any) => iss.code === "unrecognized_keys"
|
|
@@ -5982,6 +5990,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
5982
5990
|
if (args.include_archived) q.set("include_archived", "1");
|
|
5983
5991
|
if (args.top_level) q.set("top_level", "1");
|
|
5984
5992
|
if (args.changes) q.set("changes", "1");
|
|
5993
|
+
if (args.overdue !== undefined) q.set("overdue", args.overdue ? "1" : "0");
|
|
5994
|
+
if (args.due_within_days !== undefined) q.set("due_within_days", String(args.due_within_days));
|
|
5985
5995
|
return requirementsCall("GET", `/api/requirements?${q}`, args.network_id);
|
|
5986
5996
|
},
|
|
5987
5997
|
);
|
|
@@ -6133,6 +6143,12 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6133
6143
|
},
|
|
6134
6144
|
async ({ id, text, network_id }) => requirementsCall("POST", `/api/requirements/${encodeURIComponent(id)}/comments`, network_id, { text }),
|
|
6135
6145
|
);
|
|
6146
|
+
|
|
6147
|
+
// #478:tools/list 只列这个调用者真能用的工具(tool-audience.ts)。只滤列表,tools/call 不变。
|
|
6148
|
+
const caller: ToolCaller = callerTokenIsNetwork
|
|
6149
|
+
? { kind: "node", tokenId: callerTokenId ?? null, networkId: enforceNetworkId ?? null, includeProtocol: listing.includeProtocol === true }
|
|
6150
|
+
: enforceUserId ? { kind: "user", includeProtocol: listing.includeProtocol === true } : { kind: "unknown" };
|
|
6151
|
+
scopeToolsList(server, listedToolFilter(caller));
|
|
6136
6152
|
}
|
|
6137
6153
|
|
|
6138
6154
|
// ────────────────────────────────────────────────────────────────────
|
|
@@ -6164,7 +6180,7 @@ export interface UpsertNodeWithSec1GuardInput {
|
|
|
6164
6180
|
}
|
|
6165
6181
|
export type UpsertNodeOutcome =
|
|
6166
6182
|
| { result: "inserted" | "updated"; node_id: string }
|
|
6167
|
-
| { result: "refused"; reason: "cross_network" | "token_node_mismatch" | "owner_mismatch"; existingNet: string | null; callerNet: string | null }
|
|
6183
|
+
| { result: "refused"; reason: "cross_network" | "token_node_mismatch" | "owner_mismatch" | "identity_mismatch"; existingNet: string | null; callerNet: string | null }
|
|
6168
6184
|
| { result: "skipped"; reason: "missing_node_id" };
|
|
6169
6185
|
|
|
6170
6186
|
const _norm = (x: string | null | undefined) => (x === null || x === undefined ? "default" : x);
|
|
@@ -6193,8 +6209,8 @@ export function trustedConfigSnapshotForNode(
|
|
|
6193
6209
|
|
|
6194
6210
|
export function upsertNodeWithSec1Guard(input: UpsertNodeWithSec1GuardInput): UpsertNodeOutcome {
|
|
6195
6211
|
if (!input.node_id) return { result: "skipped", reason: "missing_node_id" };
|
|
6196
|
-
const existing = db.get<{ network_id: string | null; owner_user_id: string | null }>(
|
|
6197
|
-
"SELECT network_id, owner_user_id FROM nodes WHERE node_id = ?1",
|
|
6212
|
+
const existing = db.get<{ network_id: string | null; owner_user_id: string | null; alias: string | null; config_path: string | null }>(
|
|
6213
|
+
"SELECT network_id, owner_user_id, alias, config_path FROM nodes WHERE node_id = ?1",
|
|
6198
6214
|
input.node_id,
|
|
6199
6215
|
);
|
|
6200
6216
|
const callerNet = input.callerNetworkId;
|
|
@@ -6217,6 +6233,20 @@ export function upsertNodeWithSec1Guard(input: UpsertNodeWithSec1GuardInput): Up
|
|
|
6217
6233
|
if (existing?.owner_user_id && input.callerUserId !== existing.owner_user_id) {
|
|
6218
6234
|
return { result: "refused", reason: "owner_mismatch", existingNet: existing.network_id, callerNet };
|
|
6219
6235
|
}
|
|
6236
|
+
// A node_id copied into another node's config (or a hand-backfilled id that
|
|
6237
|
+
// collides) must not relabel the row: the upsert below would COALESCE the
|
|
6238
|
+
// reporter's alias over the owner's. A legit rename moves nodes.alias itself
|
|
6239
|
+
// (rename.ts), so "different alias AND a different config file" is never the
|
|
6240
|
+
// same node. Unowned legacy rows are exactly where this collision lives.
|
|
6241
|
+
if (
|
|
6242
|
+
existing?.alias && input.alias && existing.alias !== input.alias
|
|
6243
|
+
&& existing.config_path && input.config_path && existing.config_path !== input.config_path
|
|
6244
|
+
) {
|
|
6245
|
+
console.warn(
|
|
6246
|
+
`[commhub] 🚫 report_status node upsert refused (identity_mismatch): node_id=${input.node_id} row-alias=${existing.alias} reporter-alias=${input.alias}`,
|
|
6247
|
+
);
|
|
6248
|
+
return { result: "refused", reason: "identity_mismatch", existingNet: existing.network_id, callerNet };
|
|
6249
|
+
}
|
|
6220
6250
|
|
|
6221
6251
|
// Legacy / first-write paths: row missing OR network_id NULL → claim.
|
|
6222
6252
|
const isLegacy = !existing
|