@sleep2agi/commhub-server 0.9.0-preview.116 → 0.9.0-preview.118
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/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams.ts +22 -0
- package/src/node-permissions.ts +2 -1
- package/src/schedule-agent-mcp-http.test.ts +66 -0
- package/src/schedule-agent.ts +20 -1
- package/src/tool-audience-http.test.ts +1 -0
- package/src/tool-audience.ts +2 -2
- package/src/tools.ts +9 -2
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.118",
|
|
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,98 @@
|
|
|
1
|
+
import { beforeAll, afterAll, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
process.env.COMMHUB_DB ||= `${mkdtempSync('/tmp/team-whoami-')}/hub.db`;
|
|
5
|
+
let db: any, hub: any, base: string, owner: any, outside: any;
|
|
6
|
+
let selfToken: string, otherToken: string, outsideToken: string, unboundToken: string;
|
|
7
|
+
const SELF = 'node_team_whoami_self', OTHER = 'node_team_whoami_other', FOREIGN = 'node_team_whoami_foreign';
|
|
8
|
+
const TEAM = 'team_whoami_child', ROOT = 'team_whoami_root';
|
|
9
|
+
|
|
10
|
+
async function rpc(token: string, method = 'tools/call', params: unknown = { name: 'org_whoami', arguments: {} }) {
|
|
11
|
+
const response = await fetch(`${base}/mcp`, { method: 'POST', headers: {
|
|
12
|
+
Authorization: `Bearer ${token}`, 'Content-Type': 'application/json',
|
|
13
|
+
Accept: 'application/json, text/event-stream', 'MCP-Protocol-Version': '2025-03-26',
|
|
14
|
+
}, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
|
|
15
|
+
expect(response.status).toBe(200);
|
|
16
|
+
const text = await response.text();
|
|
17
|
+
const line = text.split('\n').filter(x => x.startsWith('data:')).at(-1);
|
|
18
|
+
const body = JSON.parse(line ? line.slice(5) : text);
|
|
19
|
+
expect(body.error).toBeUndefined();
|
|
20
|
+
return body.result;
|
|
21
|
+
}
|
|
22
|
+
async function who(token = selfToken, args: Record<string, unknown> = {}) {
|
|
23
|
+
const result = await rpc(token, 'tools/call', { name: 'org_whoami', arguments: args });
|
|
24
|
+
return JSON.parse(result.content[0].text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
({ db } = await import('./db.js'));
|
|
29
|
+
const { register, createNetworkTokenForNode } = await import('./auth.js');
|
|
30
|
+
owner = register('team_whoami_owner', 'Fixture-Passw0rd!xyz');
|
|
31
|
+
outside = register('team_whoami_outside', 'Fixture-Passw0rd!xyz');
|
|
32
|
+
db.run("UPDATE users SET display_name = 'Team owner' WHERE user_id = ?1", [owner.user.user_id]);
|
|
33
|
+
const mint = (u: any, alias: string, node?: string) => {
|
|
34
|
+
const r = createNetworkTokenForNode(u.user.user_id, u.network_id, alias, node);
|
|
35
|
+
expect(r.error ?? null).toBe(null); return r.token!;
|
|
36
|
+
};
|
|
37
|
+
selfToken = mint(owner, 'team-self', SELF);
|
|
38
|
+
otherToken = mint(owner, 'team-other', OTHER);
|
|
39
|
+
outsideToken = mint(outside, 'team-self', FOREIGN); // Same alias in a different network.
|
|
40
|
+
unboundToken = mint(owner, 'team-unregistered');
|
|
41
|
+
const { bootServer } = await import('./server.js');
|
|
42
|
+
hub = bootServer({ port: 0, hostname: '127.0.0.1' });
|
|
43
|
+
base = `http://127.0.0.1:${hub.port}`;
|
|
44
|
+
for (const [net, id, name, parent] of [
|
|
45
|
+
[owner.network_id, ROOT, 'Parent', null], [owner.network_id, TEAM, 'My team', ROOT],
|
|
46
|
+
[outside.network_id, TEAM, 'Foreign team', null],
|
|
47
|
+
]) db.run('INSERT INTO network_agent_teams(network_id,team_id,name,parent_id) VALUES(?1,?2,?3,?4)', [net,id,name,parent]);
|
|
48
|
+
db.run('UPDATE network_agent_teams SET lead_node_id=?1,owner_user_id=?2 WHERE network_id=?3 AND team_id=?4', [SELF,owner.user.user_id,owner.network_id,TEAM]);
|
|
49
|
+
for (const [net, node] of [[owner.network_id, SELF], [outside.network_id, FOREIGN]]) {
|
|
50
|
+
db.run('INSERT INTO network_agent_team_members(network_id,node_id,team_id) VALUES(?1,?2,?3)', [net,node,TEAM]);
|
|
51
|
+
}
|
|
52
|
+
// Even a stale/corrupt membership must not project a node from another network.
|
|
53
|
+
db.run('INSERT INTO network_agent_team_members(network_id,node_id,team_id) VALUES(?1,?2,?3)', [owner.network_id,FOREIGN,TEAM]);
|
|
54
|
+
}, 60_000);
|
|
55
|
+
afterAll(() => hub?.stop(true));
|
|
56
|
+
|
|
57
|
+
test('exact node team, nearest-first ancestors, Agent lead and private human display name', async () => {
|
|
58
|
+
const r = await who();
|
|
59
|
+
expect(r.ok).toBe(true); expect(r.source).toBe('node'); expect(r.node_id).toBe(SELF);
|
|
60
|
+
expect(r.team).toEqual({ id: TEAM, name: 'My team' });
|
|
61
|
+
expect(r.ancestors).toEqual([{ id: ROOT, name: 'Parent' }]);
|
|
62
|
+
expect(r.lead.node_id).toBe(SELF);
|
|
63
|
+
expect(r.owner).toEqual({ user_id: owner.user.user_id, display_name: 'Team owner' });
|
|
64
|
+
expect(r.agents.map((n: any) => n.node_id)).toEqual([SELF]);
|
|
65
|
+
expect(r.truncated).toBe(false);
|
|
66
|
+
expect(JSON.stringify(r)).not.toContain('team_whoami_owner');
|
|
67
|
+
});
|
|
68
|
+
test('same-id foreign team and foreign node cannot cross network; supplied identity is ignored', async () => {
|
|
69
|
+
const r = await who(selfToken, { network_id: outside.network_id, node_id: FOREIGN, alias: 'team-other' });
|
|
70
|
+
expect(r.node_id).toBe(SELF); expect(r.team.name).toBe('My team');
|
|
71
|
+
expect(JSON.stringify(r)).not.toContain(FOREIGN);
|
|
72
|
+
const b = await who(outsideToken);
|
|
73
|
+
expect(b.team.name).toBe('Foreign team'); expect(b.node_id).toBe(FOREIGN);
|
|
74
|
+
expect(b.ancestors).toEqual([]); expect(JSON.stringify(b)).not.toContain(SELF);
|
|
75
|
+
});
|
|
76
|
+
test('unassigned node does not inherit its owner or another node team', async () => {
|
|
77
|
+
expect(await who(otherToken)).toEqual({ ok: true, source: 'none', node_id: OTHER, team: null,
|
|
78
|
+
ancestors: [], lead: null, owner: null, agents: [], truncated: false });
|
|
79
|
+
expect((await who(unboundToken)).error).toBe('node_identity_unbound');
|
|
80
|
+
});
|
|
81
|
+
test('user credentials cannot list or call; node credentials can list', async () => {
|
|
82
|
+
expect((await rpc(owner.token, 'tools/list', {})).tools.some((t: any) => t.name === 'org_whoami')).toBe(false);
|
|
83
|
+
expect((await who(owner.token)).error).toBe('network_token_required');
|
|
84
|
+
expect((await rpc(selfToken, 'tools/list', {})).tools.some((t: any) => t.name === 'org_whoami')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
test('foreign lead and human owner references are not returned', async () => {
|
|
87
|
+
db.run('UPDATE network_agent_teams SET lead_node_id=?1,owner_user_id=?2 WHERE network_id=?3 AND team_id=?4', [FOREIGN,outside.user.user_id,owner.network_id,TEAM]);
|
|
88
|
+
try { const r = await who(); expect(r.lead).toBe(null); expect(r.owner).toBe(null); }
|
|
89
|
+
finally { db.run('UPDATE network_agent_teams SET lead_node_id=?1,owner_user_id=?2 WHERE network_id=?3 AND team_id=?4', [SELF,owner.user.user_id,owner.network_id,TEAM]); }
|
|
90
|
+
});
|
|
91
|
+
test('large teams are bounded to 50 and explicitly truncated', async () => {
|
|
92
|
+
for (let i = 0; i < 51; i++) {
|
|
93
|
+
const id = `node_team_whoami_cap_${i}`;
|
|
94
|
+
db.run('INSERT INTO nodes(node_id,node_name,alias,network_id) VALUES(?1,?1,?1,?2)', [id,owner.network_id]);
|
|
95
|
+
db.run('INSERT INTO network_agent_team_members(network_id,node_id,team_id) VALUES(?1,?2,?3)', [owner.network_id,id,TEAM]);
|
|
96
|
+
}
|
|
97
|
+
const r = await who(); expect(r.agents.length).toBe(50); expect(r.truncated).toBe(true);
|
|
98
|
+
});
|
package/src/agent-teams.ts
CHANGED
|
@@ -125,6 +125,28 @@ export function listAgentTeams(networkId: string, hidden: Hidden) {
|
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** #765: node-token identity only; no fallback to a human's department.
|
|
129
|
+
* Reuse the REST projection's network joins and display-name privacy rules.
|
|
130
|
+
*/
|
|
131
|
+
export function agentTeamWhoami(networkId: string, nodeId: string | null) {
|
|
132
|
+
if (!nodeId) return { ok: false, error: "node_identity_unbound" };
|
|
133
|
+
const teams = listAgentTeams(networkId, () => false);
|
|
134
|
+
const self = teams.find(t => t.members.some(n => n.node_id === nodeId));
|
|
135
|
+
if (!self) return { ok: true, source: "none", node_id: nodeId, team: null, ancestors: [], lead: null, owner: null, agents: [], truncated: false };
|
|
136
|
+
const byId = new Map(teams.map(t => [t.id, t]));
|
|
137
|
+
const seen = new Set([self.id]);
|
|
138
|
+
const ancestors: { id: string; name: string }[] = [];
|
|
139
|
+
for (let id = self.parent_id; id && !seen.has(id);) {
|
|
140
|
+
const parent = byId.get(id);
|
|
141
|
+
if (!parent) break;
|
|
142
|
+
seen.add(id);
|
|
143
|
+
ancestors.push({ id, name: parent.name });
|
|
144
|
+
id = parent.parent_id;
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, source: "node", node_id: nodeId, team: { id: self.id, name: self.name }, ancestors,
|
|
147
|
+
lead: self.lead, owner: self.owner, agents: self.members.slice(0, 50), truncated: self.members.length > 50 };
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
/** scope = null 表示全管;否则是调用者能管的团队集合。 */
|
|
129
151
|
export function createAgentTeam(networkId: string, actor: string, scope: Set<string> | null, hidden: Hidden, body: Record<string, unknown>) {
|
|
130
152
|
const name = cleanName(body.name);
|
package/src/node-permissions.ts
CHANGED
|
@@ -257,7 +257,7 @@ export const NODE_TOOL_CLASS: Readonly<Record<string, ToolClass>> = {
|
|
|
257
257
|
get_adopt_request: "always", ack_adopt_request: "always",
|
|
258
258
|
send_desktop_message: "always", // 发给人的私信,不是派活
|
|
259
259
|
// 读
|
|
260
|
-
get_all_status: "read", get_session_status: "read", get_task: "read", list_tasks: "read", get_completions: "read",
|
|
260
|
+
get_all_status: "read", get_session_status: "read", get_task: "read", list_tasks: "read", get_completions: "read", org_whoami: "read",
|
|
261
261
|
list_skills: "read", get_skill: "read", list_host_supervisors: "read", list_network_secrets: "read", list_providers: "read",
|
|
262
262
|
get_probe_results: "read", read_node_rules_file: "read", list_node_skills: "read", read_node_skill: "read", list_node_files: "read",
|
|
263
263
|
read_node_file: "read", tail_node_logs: "read",
|
|
@@ -271,6 +271,7 @@ export const NODE_TOOL_CLASS: Readonly<Record<string, ToolClass>> = {
|
|
|
271
271
|
// 定时任务(#733):在 schedule-agent.ts 里按排程判(范围 / 目标 = send_task 的 dispatchVerdict / 配额)
|
|
272
272
|
schedule_list: "read", schedule_get: "read", schedule_runs: "read",
|
|
273
273
|
schedule_create: "schedule", schedule_update: "schedule", schedule_cancel: "schedule", schedule_run_now: "schedule",
|
|
274
|
+
schedule_batch_interval: "schedule",
|
|
274
275
|
// 写节点 / 技能 / 探测:只读、受限不行;正常由 RFC-036 等既有规则管
|
|
275
276
|
update_node_config: "node_write", write_node_rules_file: "node_write", restart_node: "node_write", create_node: "node_write",
|
|
276
277
|
stop_node: "node_write", delete_node: "node_write", start_node: "node_write", submit_skill: "node_write", probe_provider_model: "node_write",
|
|
@@ -161,6 +161,72 @@ describe("happy path from a node", () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
describe("batch interval updates (#816)", () => {
|
|
165
|
+
const batch = (token: string, schedule_ids: string[], every_seconds = 600) =>
|
|
166
|
+
mcp(token, "schedule_batch_interval", { schedule_ids, every_seconds });
|
|
167
|
+
const get = async (id: string) => (await mcp(node.b.token, "schedule_get", { schedule_id: id })).schedule;
|
|
168
|
+
|
|
169
|
+
test("two updates, deduplicated IDs, paused stays paused, unselected stays untouched", async () => {
|
|
170
|
+
const a = (await create("b")).schedule, b = (await create("b")).schedule;
|
|
171
|
+
const untouched = (await create("b")).schedule;
|
|
172
|
+
await mcp(node.b.token, "schedule_update", { schedule_id: b.schedule_id, status: "paused" });
|
|
173
|
+
const before = Date.now();
|
|
174
|
+
const result = await batch(node.b.token, [a.schedule_id, b.schedule_id, a.schedule_id]);
|
|
175
|
+
expect(result).toMatchObject({ ok: true, updated: 2, failed: 0 });
|
|
176
|
+
expect(result.results).toHaveLength(2);
|
|
177
|
+
expect(result.results[0]).toMatchObject({ schedule_id: a.schedule_id, every_seconds: 600, revision: a.revision + 1, status: "active" });
|
|
178
|
+
expect(Date.parse(result.results[0].next_run_at)).toBeGreaterThanOrEqual(before + 600_000);
|
|
179
|
+
expect(result.results[1]).toMatchObject({ status: "paused", next_run_at: null, every_seconds: 600 });
|
|
180
|
+
for (const original of [a, b]) {
|
|
181
|
+
expect(await get(original.schedule_id)).toMatchObject({ name: original.name, task_content: original.task_content, target_node_id: original.target_node_id, schedule: { type: "interval", every_seconds: 600 } });
|
|
182
|
+
}
|
|
183
|
+
expect(await get(untouched.schedule_id)).toEqual(untouched);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("partial results preserve human, foreign-network, daily and cancelled schedules", async () => {
|
|
187
|
+
const own = (await create("b")).schedule;
|
|
188
|
+
const daily = (await create("b", { schedule: { type: "daily", time: "09:30" } })).schedule;
|
|
189
|
+
const cancelled = (await create("b")).schedule;
|
|
190
|
+
await mcp(node.b.token, "schedule_cancel", { schedule_id: cancelled.schedule_id });
|
|
191
|
+
const human = await rest(user.boss.token, "POST", "/api/scheduled-tasks", { network_id: net, name: "human batch", task: "t", target_node_id: node.b.id, schedule: EVERY_MIN });
|
|
192
|
+
const foreign = (await create("x")).schedule;
|
|
193
|
+
const ids = [human.body.schedule.schedule_id, foreign.schedule_id, "no-such-schedule", daily.schedule_id, cancelled.schedule_id, own.schedule_id];
|
|
194
|
+
const result = await batch(node.b.token, ids);
|
|
195
|
+
expect(result).toMatchObject({ ok: false, updated: 1, failed: 5 });
|
|
196
|
+
expect(result.results.map((r: any) => r.error ?? "ok")).toEqual(["not_schedule_creator", "schedule_not_found", "schedule_not_found", "not_interval_schedule", "schedule_cancelled", "ok"]);
|
|
197
|
+
expect((await get(ids[0])).schedule).toEqual(EVERY_MIN);
|
|
198
|
+
expect((await mcp(node.x.token, "schedule_get", { schedule_id: foreign.schedule_id })).schedule).toEqual(foreign);
|
|
199
|
+
expect(await get(daily.schedule_id)).toEqual(daily);
|
|
200
|
+
expect((await get(cancelled.schedule_id)).status).toBe("cancelled");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("invalid interval or batch shape performs no writes; node-only and readonly gates remain", async () => {
|
|
204
|
+
const own = (await create("b")).schedule, ids = [own.schedule_id];
|
|
205
|
+
for (const seconds of [59, 60.5, 365 * 86400 + 1]) {
|
|
206
|
+
expect(await batch(node.b.token, ids, seconds)).toMatchObject({ ok: false, error: "invalid_interval" });
|
|
207
|
+
}
|
|
208
|
+
for (const invalid of [[], Array(101).fill(own.schedule_id), [""]]) {
|
|
209
|
+
expect((await batch(node.b.token, invalid)).ok).not.toBe(true);
|
|
210
|
+
}
|
|
211
|
+
expect((await batch(user.boss.token, ids)).error).toBe("network_token_required");
|
|
212
|
+
expect(await batch(node.ro.token, ids)).toMatchObject({ error: "node_permission_denied", reason: "mode_readonly" });
|
|
213
|
+
expect(await get(own.schedule_id)).toEqual(own);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("creator still needs permission to reach the current target", async () => {
|
|
217
|
+
const own = (await create("b", { target_node_id: node.a.id })).schedule;
|
|
218
|
+
db.run("UPDATE nodes SET owner_user_id = ?1 WHERE node_id = ?2", [user.rst.id, node.b.id]);
|
|
219
|
+
try {
|
|
220
|
+
const result = await batch(node.b.token, [own.schedule_id]);
|
|
221
|
+
expect(result).toMatchObject({ ok: false, updated: 0, failed: 1 });
|
|
222
|
+
expect(result.results[0]).toMatchObject({ reason: "agent_not_granted_to_owner" });
|
|
223
|
+
expect(await get(own.schedule_id)).toEqual(own);
|
|
224
|
+
} finally {
|
|
225
|
+
db.run("UPDATE nodes SET owner_user_id = ?1 WHERE node_id = ?2", [user.boss.id, node.b.id]);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
164
230
|
describe("denials", () => {
|
|
165
231
|
test("another network: cannot target, see or touch it", async () => {
|
|
166
232
|
const mine = (await create("a")).schedule.schedule_id;
|
package/src/schedule-agent.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { db } from "./db.js";
|
|
24
24
|
import {
|
|
25
25
|
cancelSchedule, decodeRowForAgent, dispatchScheduledOccurrence, getScheduleRow, insertSchedule, patchSchedule,
|
|
26
|
-
scheduleRuns, ScheduleInputError, validateScheduleCreate, type ScheduledRow,
|
|
26
|
+
scheduleRuns, ScheduleInputError, validateScheduleCreate, parseScheduleSpec, type ScheduledRow,
|
|
27
27
|
} from "./scheduled-tasks.js";
|
|
28
28
|
import { SCHEDULED_TASK_STORAGE_SELECT } from "./rest-projections.js";
|
|
29
29
|
import { dispatchVerdict, nodePermissionDeniedBody, recordNodePermission, type NodeIdentity } from "./node-permissions.js";
|
|
@@ -145,6 +145,25 @@ export function agentUpdateSchedule(id: NodeIdentity, canWrite: boolean, schedul
|
|
|
145
145
|
} catch (e) { return inputError(e); }
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** #816: partial success, one result per distinct ID; no change to existing write authority. */
|
|
149
|
+
export function agentBatchScheduleInterval(id: NodeIdentity, canWrite: boolean, scheduleIds: string[], everySeconds: number): AgentScheduleResult {
|
|
150
|
+
const gate = writeGate(id, canWrite, "mcp:schedule_batch_interval");
|
|
151
|
+
if (gate) return gate;
|
|
152
|
+
const schedule = { type: "interval", every_seconds: everySeconds };
|
|
153
|
+
try { parseScheduleSpec(schedule, "UTC"); } catch (e) { return inputError(e); }
|
|
154
|
+
const results = [...new Set(scheduleIds)].map(schedule_id => {
|
|
155
|
+
const row = ownRow(id, schedule_id);
|
|
156
|
+
if (!isRow(row)) return { schedule_id, ...row };
|
|
157
|
+
if (row.schedule_type !== "interval") return { schedule_id, ok: false, error: "not_interval_schedule" };
|
|
158
|
+
const result = agentUpdateSchedule(id, canWrite, schedule_id, { schedule, base_revision: row.revision });
|
|
159
|
+
if (!result.ok) return { schedule_id, ...result };
|
|
160
|
+
const updated = result.schedule as ReturnType<typeof decodeRowForAgent>;
|
|
161
|
+
return { schedule_id, ok: true, every_seconds: everySeconds, next_run_at: updated.next_run_at, status: updated.status, revision: updated.revision };
|
|
162
|
+
});
|
|
163
|
+
const updated = results.filter(r => r.ok).length;
|
|
164
|
+
return { ok: updated === results.length, updated, failed: results.length - updated, results };
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
export function agentCancelSchedule(id: NodeIdentity, canWrite: boolean, scheduleId: string): AgentScheduleResult {
|
|
149
168
|
const gate = writeGate(id, canWrite, "mcp:schedule_cancel");
|
|
150
169
|
if (gate) return gate;
|
|
@@ -43,6 +43,7 @@ const OVERRIDES: Record<string, Record<string, unknown>> = {
|
|
|
43
43
|
mark_tasks_consumed: { task_ids: ["task_probe"] },
|
|
44
44
|
upsert_network_secret: { key: "PROBE_KEY" },
|
|
45
45
|
update_provider: { provider_id: "prov_probe" },
|
|
46
|
+
schedule_batch_interval: { schedule_ids: ["sched_probe"], every_seconds: 60 },
|
|
46
47
|
};
|
|
47
48
|
function fill(schema: any, key: string, ctx: Record<string, unknown>): unknown {
|
|
48
49
|
if (!schema) return "probe";
|
package/src/tool-audience.ts
CHANGED
|
@@ -26,10 +26,10 @@ export const TOOL_AUDIENCE: Readonly<Record<string, ToolAudience>> = {
|
|
|
26
26
|
request_adopt_node: "user", unadopt_node: "user",
|
|
27
27
|
get_adopt_request: "protocol", ack_adopt_request: "protocol",
|
|
28
28
|
// ── 节点:用户令牌一律 network_token_required ──
|
|
29
|
-
report_status: "node",
|
|
29
|
+
report_status: "node", org_whoami: "node",
|
|
30
30
|
// #733 定时任务:节点工具(用户令牌一律 network_token_required;人走 REST /api/scheduled-tasks)
|
|
31
31
|
schedule_list: "node", schedule_get: "node", schedule_create: "node", schedule_update: "node",
|
|
32
|
-
schedule_cancel: "node", schedule_run_now: "node", schedule_runs: "node",
|
|
32
|
+
schedule_cancel: "node", schedule_run_now: "node", schedule_runs: "node", schedule_batch_interval: "node",
|
|
33
33
|
// ── 协议:用户令牌一律 network_token_required / node_token_required / caller_not_a_daemon ──
|
|
34
34
|
mark_tasks_runtime_submitted: "protocol", mark_tasks_consumed: "protocol",
|
|
35
35
|
get_config_update: "protocol", ack_config_update: "protocol",
|
package/src/tools.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { requestAdopt, getAdopt, ackAdopt, unadopt, resolveManagedDaemon, activeBinding, createdDaemon } from "./node-daemon-bindings.js";
|
|
3
3
|
import { runtimeReadinessSchema } from "./runtime-readiness.js";
|
|
4
4
|
import { lockNodeAlias } from "./node-token-ownership.js";
|
|
5
|
-
import { clearNodeFromAgentTeams } from "./agent-teams.js";
|
|
5
|
+
import { clearNodeFromAgentTeams, agentTeamWhoami } from "./agent-teams.js";
|
|
6
6
|
import { parseDbTimestampMs } from "./db-timestamp.js";
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
8
|
import { nodeHealthSchema, normalizeNodeHealth, recordNodeHealth } from "./node-health-store.js";
|
|
@@ -21,7 +21,7 @@ import { getUserNetworkRole, createNetworkTokenForNode } from "./auth.js";
|
|
|
21
21
|
import { addAgentNetworkScope, addAgentTimelineScope, addNetworkScope, canRestWriteNetwork, canRestWriteNetworkAsHuman, getUserNetworkIds, resolveRestNetworkScope, singleNetworkId, type RestNetworkScope } from "./network-scope.js";
|
|
22
22
|
import { canMessageAgent, restrictedNetworkIds, RESTRICTED_MEMBER_TOOLS, type AgentRef } from "./agent-access.js";
|
|
23
23
|
import { listedToolFilter, scopeToolsList, type ToolCaller } from "./tool-audience.js";
|
|
24
|
-
import { agentCancelSchedule, agentCreateSchedule, agentGetSchedule, agentListSchedules, agentRunScheduleNow, agentScheduleRuns, agentUpdateSchedule } from "./schedule-agent.js";
|
|
24
|
+
import { agentBatchScheduleInterval, agentCancelSchedule, agentCreateSchedule, agentGetSchedule, agentListSchedules, agentRunScheduleNow, agentScheduleRuns, agentUpdateSchedule } from "./schedule-agent.js";
|
|
25
25
|
import { broadcastVerdict, dispatchVerdict, humanOnlyVerdict, NODE_TOOL_CLASS, nodeDecide, nodeIdentity, nodePermissionDeniedBody, writeVerdict, type NodeIdentity, type Verdict } from "./node-permissions.js";
|
|
26
26
|
import { restrictedMemberAttachmentsDenied } from "./restricted-files.js";
|
|
27
27
|
import { errorBody } from "./requirements-errors.js";
|
|
@@ -6396,6 +6396,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6396
6396
|
server.tool("schedule_update", "Edit your schedule; status=paused|active.",
|
|
6397
6397
|
{ ...schedFields, schedule_id: schedId, base_revision: z.number().int().min(0).optional().describe("Reject if current revision differs."), name: z.string().optional(), task: z.string().optional(), schedule: z.any().optional(), status: z.string().optional() },
|
|
6398
6398
|
async ({ schedule_id, ...rest }) => scheduleCall((id, w) => agentUpdateSchedule(id, w, schedule_id, rest as Record<string, unknown>)));
|
|
6399
|
+
server.tool("schedule_batch_interval", "Batch interval edit; per-ID results.",
|
|
6400
|
+
{ schedule_ids: z.array(z.string()).min(1).max(100), every_seconds: z.number() },
|
|
6401
|
+
async ({ schedule_ids, every_seconds }) => scheduleCall((id, w) => agentBatchScheduleInterval(id, w, schedule_ids, every_seconds)));
|
|
6399
6402
|
server.tool("schedule_cancel", "Cancel your schedule.", { schedule_id: schedId },
|
|
6400
6403
|
async ({ schedule_id }) => scheduleCall((id, w) => agentCancelSchedule(id, w, schedule_id)));
|
|
6401
6404
|
server.tool("schedule_run_now", "Run your schedule now.", { schedule_id: schedId },
|
|
@@ -6403,6 +6406,10 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
6403
6406
|
server.tool("schedule_runs", "A schedule's recent runs.", { schedule_id: schedId },
|
|
6404
6407
|
async ({ schedule_id }) => scheduleCall(id => agentScheduleRuns(id, schedule_id)));
|
|
6405
6408
|
|
|
6409
|
+
// #765: exact authenticated node; team tree is independent of human departments.
|
|
6410
|
+
server.tool("org_whoami", "Your Agent team, ancestors, lead, owner and up to 50 teammates. Read-only.", {},
|
|
6411
|
+
async () => scheduleCall(id => agentTeamWhoami(id.networkId, id.nodeId)));
|
|
6412
|
+
|
|
6406
6413
|
// #478:tools/list 只列这个调用者真能用的工具(tool-audience.ts)。只滤列表,tools/call 不变。
|
|
6407
6414
|
const caller: ToolCaller = callerTokenIsNetwork
|
|
6408
6415
|
? { kind: "node", tokenId: callerTokenId ?? null, networkId: enforceNetworkId ?? null, includeProtocol: listing.includeProtocol === true }
|