@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.
@@ -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
- registerTools(server, undefined, "net_list_tools", "u_list_tools", "u_list_tools", false, "tok_list_tools");
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