@naisys/supervisor 3.0.0-beta.10
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/bin/naisys-supervisor +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -0
- package/client-dist/assets/index-WzoDF0aQ.js +177 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +49 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +54 -0
- package/dist/auth-middleware.js +116 -0
- package/dist/database/hubDb.js +26 -0
- package/dist/database/supervisorDb.js +18 -0
- package/dist/error-helpers.js +13 -0
- package/dist/hateoas.js +61 -0
- package/dist/logger.js +11 -0
- package/dist/route-helpers.js +7 -0
- package/dist/routes/admin.js +209 -0
- package/dist/routes/agentChat.js +194 -0
- package/dist/routes/agentConfig.js +265 -0
- package/dist/routes/agentLifecycle.js +350 -0
- package/dist/routes/agentMail.js +171 -0
- package/dist/routes/agentRuns.js +90 -0
- package/dist/routes/agents.js +236 -0
- package/dist/routes/api.js +52 -0
- package/dist/routes/attachments.js +18 -0
- package/dist/routes/auth.js +103 -0
- package/dist/routes/costs.js +51 -0
- package/dist/routes/hosts.js +296 -0
- package/dist/routes/models.js +152 -0
- package/dist/routes/root.js +56 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/status.js +20 -0
- package/dist/routes/users.js +420 -0
- package/dist/routes/variables.js +103 -0
- package/dist/schema-registry.js +23 -0
- package/dist/services/agentConfigService.js +182 -0
- package/dist/services/agentHostStatusService.js +178 -0
- package/dist/services/agentService.js +291 -0
- package/dist/services/attachmentProxyService.js +131 -0
- package/dist/services/browserSocketService.js +78 -0
- package/dist/services/chatService.js +201 -0
- package/dist/services/configExportService.js +61 -0
- package/dist/services/costsService.js +127 -0
- package/dist/services/hostService.js +156 -0
- package/dist/services/hubConnectionService.js +320 -0
- package/dist/services/logFileService.js +11 -0
- package/dist/services/mailService.js +154 -0
- package/dist/services/modelService.js +92 -0
- package/dist/services/runsService.js +168 -0
- package/dist/services/userService.js +147 -0
- package/dist/services/variableService.js +23 -0
- package/dist/supervisorServer.js +221 -0
- package/package.json +79 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { AgentActionResultSchema, AgentStartRequestSchema, AgentStartResultSchema, AgentStopRequestSchema, AgentStopResultSchema, AgentToggleRequestSchema, AgentUsernameParamsSchema, ErrorResponseSchema, SetLeadAgentRequestSchema, } from "@naisys/supervisor-shared";
|
|
2
|
+
import { requirePermission } from "../auth-middleware.js";
|
|
3
|
+
import { hubDb } from "../database/hubDb.js";
|
|
4
|
+
import { badRequest, notFound } from "../error-helpers.js";
|
|
5
|
+
import { isAgentActive } from "../services/agentHostStatusService.js";
|
|
6
|
+
import { archiveAgent, deleteAgent, disableAgent, enableAgent, getAgent, resetAgentSpend, resolveAgentId, unarchiveAgent, updateLeadAgent, } from "../services/agentService.js";
|
|
7
|
+
import { isHubConnected, sendAgentStart, sendAgentStop, sendUserListChanged, } from "../services/hubConnectionService.js";
|
|
8
|
+
async function findSubordinates(parentUserId, filter) {
|
|
9
|
+
const allUsers = await hubDb.users.findMany({
|
|
10
|
+
select: { id: true, lead_user_id: true },
|
|
11
|
+
});
|
|
12
|
+
const result = [];
|
|
13
|
+
function collect(parentId) {
|
|
14
|
+
for (const user of allUsers) {
|
|
15
|
+
if (user.lead_user_id === parentId) {
|
|
16
|
+
if (!filter || filter(user.id)) {
|
|
17
|
+
result.push(user.id);
|
|
18
|
+
}
|
|
19
|
+
collect(user.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
collect(parentUserId);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export default function agentLifecycleRoutes(fastify, _options) {
|
|
27
|
+
// POST /:username/start — Start agent via hub
|
|
28
|
+
fastify.post("/:username/start", {
|
|
29
|
+
preHandler: [requirePermission("manage_agents")],
|
|
30
|
+
schema: {
|
|
31
|
+
description: "Start an agent via the hub",
|
|
32
|
+
tags: ["Agents"],
|
|
33
|
+
params: AgentUsernameParamsSchema,
|
|
34
|
+
body: AgentStartRequestSchema,
|
|
35
|
+
response: {
|
|
36
|
+
200: AgentStartResultSchema,
|
|
37
|
+
503: ErrorResponseSchema,
|
|
38
|
+
500: ErrorResponseSchema,
|
|
39
|
+
},
|
|
40
|
+
security: [{ cookieAuth: [] }],
|
|
41
|
+
},
|
|
42
|
+
}, async (request, reply) => {
|
|
43
|
+
const { username } = request.params;
|
|
44
|
+
const { task } = request.body;
|
|
45
|
+
const id = resolveAgentId(username);
|
|
46
|
+
if (!id) {
|
|
47
|
+
return notFound(reply, "Agent not found");
|
|
48
|
+
}
|
|
49
|
+
if (!isHubConnected()) {
|
|
50
|
+
return reply.status(503).send({
|
|
51
|
+
success: false,
|
|
52
|
+
message: "Hub is not connected",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const naisysUser = (await hubDb.users.findFirst({
|
|
56
|
+
where: { uuid: request.supervisorUser.uuid },
|
|
57
|
+
select: { id: true },
|
|
58
|
+
})) ??
|
|
59
|
+
(await hubDb.users.findFirst({
|
|
60
|
+
where: { username: "admin" },
|
|
61
|
+
select: { id: true },
|
|
62
|
+
}));
|
|
63
|
+
if (!naisysUser) {
|
|
64
|
+
return reply.status(500).send({
|
|
65
|
+
success: false,
|
|
66
|
+
message: "No matching user found in NAISYS database",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const response = await sendAgentStart(id, task, naisysUser.id);
|
|
70
|
+
if (response.success) {
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: "Agent started",
|
|
74
|
+
hostname: response.hostname,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return reply.status(500).send({
|
|
79
|
+
success: false,
|
|
80
|
+
message: response.error || "Failed to start agent",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// POST /:username/stop — Stop agent via hub
|
|
85
|
+
fastify.post("/:username/stop", {
|
|
86
|
+
preHandler: [requirePermission("manage_agents")],
|
|
87
|
+
schema: {
|
|
88
|
+
description: "Stop an agent via the hub",
|
|
89
|
+
tags: ["Agents"],
|
|
90
|
+
params: AgentUsernameParamsSchema,
|
|
91
|
+
body: AgentStopRequestSchema,
|
|
92
|
+
response: {
|
|
93
|
+
200: AgentStopResultSchema,
|
|
94
|
+
503: ErrorResponseSchema,
|
|
95
|
+
500: ErrorResponseSchema,
|
|
96
|
+
},
|
|
97
|
+
security: [{ cookieAuth: [] }],
|
|
98
|
+
},
|
|
99
|
+
}, async (request, reply) => {
|
|
100
|
+
const { username } = request.params;
|
|
101
|
+
const { recursive } = request.body;
|
|
102
|
+
const id = resolveAgentId(username);
|
|
103
|
+
if (!id) {
|
|
104
|
+
return notFound(reply, "Agent not found");
|
|
105
|
+
}
|
|
106
|
+
if (!isHubConnected()) {
|
|
107
|
+
return reply.status(503).send({
|
|
108
|
+
success: false,
|
|
109
|
+
message: "Hub is not connected",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Fire-and-forget stops for subordinates when recursive
|
|
113
|
+
if (recursive) {
|
|
114
|
+
const subordinates = await findSubordinates(id, isAgentActive);
|
|
115
|
+
void Promise.all(subordinates.map((subId) => sendAgentStop(subId, "Stopped from supervisor (recursive)").catch((err) => request.log.error(err, `Failed to stop subordinate agent ${subId}`))));
|
|
116
|
+
}
|
|
117
|
+
const response = await sendAgentStop(id, "Stopped from supervisor");
|
|
118
|
+
if (response.success) {
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
message: recursive
|
|
122
|
+
? "Agent and subordinates stopped"
|
|
123
|
+
: "Agent stopped",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
return reply.status(500).send({
|
|
128
|
+
success: false,
|
|
129
|
+
message: response.error || "Failed to stop agent",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// POST /:username/enable — Enable agent
|
|
134
|
+
fastify.post("/:username/enable", {
|
|
135
|
+
preHandler: [requirePermission("manage_agents")],
|
|
136
|
+
schema: {
|
|
137
|
+
description: "Enable an agent",
|
|
138
|
+
tags: ["Agents"],
|
|
139
|
+
params: AgentUsernameParamsSchema,
|
|
140
|
+
body: AgentToggleRequestSchema,
|
|
141
|
+
response: {
|
|
142
|
+
200: AgentActionResultSchema,
|
|
143
|
+
400: ErrorResponseSchema,
|
|
144
|
+
500: ErrorResponseSchema,
|
|
145
|
+
},
|
|
146
|
+
security: [{ cookieAuth: [] }],
|
|
147
|
+
},
|
|
148
|
+
}, async (request, reply) => {
|
|
149
|
+
const { username } = request.params;
|
|
150
|
+
const { recursive } = request.body;
|
|
151
|
+
const id = resolveAgentId(username);
|
|
152
|
+
if (!id) {
|
|
153
|
+
return notFound(reply, "Agent not found");
|
|
154
|
+
}
|
|
155
|
+
const subordinateIds = recursive ? await findSubordinates(id) : [];
|
|
156
|
+
await enableAgent(id);
|
|
157
|
+
await Promise.all(subordinateIds.map((subId) => enableAgent(subId)));
|
|
158
|
+
sendUserListChanged();
|
|
159
|
+
const count = subordinateIds.length + 1;
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
message: recursive && count > 1
|
|
163
|
+
? `Enabled ${count} agent(s)`
|
|
164
|
+
: "Agent enabled",
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
// POST /:username/disable — Disable agent
|
|
168
|
+
fastify.post("/:username/disable", {
|
|
169
|
+
preHandler: [requirePermission("manage_agents")],
|
|
170
|
+
schema: {
|
|
171
|
+
description: "Disable an agent",
|
|
172
|
+
tags: ["Agents"],
|
|
173
|
+
params: AgentUsernameParamsSchema,
|
|
174
|
+
body: AgentToggleRequestSchema,
|
|
175
|
+
response: {
|
|
176
|
+
200: AgentActionResultSchema,
|
|
177
|
+
400: ErrorResponseSchema,
|
|
178
|
+
500: ErrorResponseSchema,
|
|
179
|
+
},
|
|
180
|
+
security: [{ cookieAuth: [] }],
|
|
181
|
+
},
|
|
182
|
+
}, async (request, reply) => {
|
|
183
|
+
const { username } = request.params;
|
|
184
|
+
const { recursive } = request.body;
|
|
185
|
+
const id = resolveAgentId(username);
|
|
186
|
+
if (!id) {
|
|
187
|
+
return notFound(reply, "Agent not found");
|
|
188
|
+
}
|
|
189
|
+
const subordinateIds = recursive ? await findSubordinates(id) : [];
|
|
190
|
+
const allIds = [id, ...subordinateIds];
|
|
191
|
+
// Disable all in DB first, then try to stop any active ones
|
|
192
|
+
await Promise.all(allIds.map((agentId) => disableAgent(agentId)));
|
|
193
|
+
sendUserListChanged();
|
|
194
|
+
if (isHubConnected()) {
|
|
195
|
+
const activeIds = allIds.filter((agentId) => isAgentActive(agentId));
|
|
196
|
+
if (activeIds.length > 0) {
|
|
197
|
+
void Promise.all(activeIds.map((agentId) => sendAgentStop(agentId, "Agent disabled").catch((err) => request.log.error(err, `Failed to stop disabled agent ${agentId}`))));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const count = allIds.length;
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
message: recursive && count > 1
|
|
204
|
+
? `Disabled ${count} agent(s); stop requested for active ones`
|
|
205
|
+
: isAgentActive(id)
|
|
206
|
+
? "Agent disabled; stop requested"
|
|
207
|
+
: "Agent disabled",
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
// POST /:username/archive — Archive agent
|
|
211
|
+
fastify.post("/:username/archive", {
|
|
212
|
+
preHandler: [requirePermission("manage_agents")],
|
|
213
|
+
schema: {
|
|
214
|
+
description: "Archive an agent",
|
|
215
|
+
tags: ["Agents"],
|
|
216
|
+
params: AgentUsernameParamsSchema,
|
|
217
|
+
response: {
|
|
218
|
+
200: AgentActionResultSchema,
|
|
219
|
+
400: ErrorResponseSchema,
|
|
220
|
+
500: ErrorResponseSchema,
|
|
221
|
+
},
|
|
222
|
+
security: [{ cookieAuth: [] }],
|
|
223
|
+
},
|
|
224
|
+
}, async (request, reply) => {
|
|
225
|
+
const { username } = request.params;
|
|
226
|
+
const id = resolveAgentId(username);
|
|
227
|
+
if (!id) {
|
|
228
|
+
return notFound(reply, "Agent not found");
|
|
229
|
+
}
|
|
230
|
+
if (isAgentActive(id)) {
|
|
231
|
+
return badRequest(reply, "Cannot archive an active agent. Stop it first.");
|
|
232
|
+
}
|
|
233
|
+
await archiveAgent(id);
|
|
234
|
+
sendUserListChanged();
|
|
235
|
+
return { success: true, message: "Agent archived" };
|
|
236
|
+
});
|
|
237
|
+
// POST /:username/unarchive — Unarchive agent
|
|
238
|
+
fastify.post("/:username/unarchive", {
|
|
239
|
+
preHandler: [requirePermission("manage_agents")],
|
|
240
|
+
schema: {
|
|
241
|
+
description: "Unarchive an agent",
|
|
242
|
+
tags: ["Agents"],
|
|
243
|
+
params: AgentUsernameParamsSchema,
|
|
244
|
+
response: {
|
|
245
|
+
200: AgentActionResultSchema,
|
|
246
|
+
400: ErrorResponseSchema,
|
|
247
|
+
500: ErrorResponseSchema,
|
|
248
|
+
},
|
|
249
|
+
security: [{ cookieAuth: [] }],
|
|
250
|
+
},
|
|
251
|
+
}, async (request, reply) => {
|
|
252
|
+
const { username } = request.params;
|
|
253
|
+
const id = resolveAgentId(username);
|
|
254
|
+
if (!id) {
|
|
255
|
+
return notFound(reply, "Agent not found");
|
|
256
|
+
}
|
|
257
|
+
await unarchiveAgent(id);
|
|
258
|
+
sendUserListChanged();
|
|
259
|
+
return { success: true, message: "Agent unarchived" };
|
|
260
|
+
});
|
|
261
|
+
// PUT /:username/lead — Set or clear lead agent
|
|
262
|
+
fastify.put("/:username/lead", {
|
|
263
|
+
preHandler: [requirePermission("manage_agents")],
|
|
264
|
+
schema: {
|
|
265
|
+
description: "Set or clear the lead agent",
|
|
266
|
+
tags: ["Agents"],
|
|
267
|
+
params: AgentUsernameParamsSchema,
|
|
268
|
+
body: SetLeadAgentRequestSchema,
|
|
269
|
+
response: {
|
|
270
|
+
200: AgentActionResultSchema,
|
|
271
|
+
400: ErrorResponseSchema,
|
|
272
|
+
500: ErrorResponseSchema,
|
|
273
|
+
},
|
|
274
|
+
security: [{ cookieAuth: [] }],
|
|
275
|
+
},
|
|
276
|
+
}, async (request, reply) => {
|
|
277
|
+
const { username } = request.params;
|
|
278
|
+
const { leadAgentUsername } = request.body;
|
|
279
|
+
const id = resolveAgentId(username);
|
|
280
|
+
if (!id) {
|
|
281
|
+
return notFound(reply, "Agent not found");
|
|
282
|
+
}
|
|
283
|
+
await updateLeadAgent(id, leadAgentUsername);
|
|
284
|
+
sendUserListChanged();
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
message: leadAgentUsername
|
|
288
|
+
? "Lead agent updated"
|
|
289
|
+
: "Lead agent cleared",
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
// POST /:username/reset-spend — Reset agent spend counter
|
|
293
|
+
fastify.post("/:username/reset-spend", {
|
|
294
|
+
preHandler: [requirePermission("manage_agents")],
|
|
295
|
+
schema: {
|
|
296
|
+
description: "Reset an agent's spend counter",
|
|
297
|
+
tags: ["Agents"],
|
|
298
|
+
params: AgentUsernameParamsSchema,
|
|
299
|
+
response: {
|
|
300
|
+
200: AgentActionResultSchema,
|
|
301
|
+
400: ErrorResponseSchema,
|
|
302
|
+
500: ErrorResponseSchema,
|
|
303
|
+
},
|
|
304
|
+
security: [{ cookieAuth: [] }],
|
|
305
|
+
},
|
|
306
|
+
}, async (request, reply) => {
|
|
307
|
+
const { username } = request.params;
|
|
308
|
+
const id = resolveAgentId(username);
|
|
309
|
+
if (!id) {
|
|
310
|
+
return notFound(reply, "Agent not found");
|
|
311
|
+
}
|
|
312
|
+
await resetAgentSpend(id);
|
|
313
|
+
return { success: true, message: "Spend counter reset" };
|
|
314
|
+
});
|
|
315
|
+
// DELETE /:username — Permanently delete agent
|
|
316
|
+
fastify.delete("/:username", {
|
|
317
|
+
preHandler: [requirePermission("manage_agents")],
|
|
318
|
+
schema: {
|
|
319
|
+
description: "Permanently delete an archived agent",
|
|
320
|
+
tags: ["Agents"],
|
|
321
|
+
params: AgentUsernameParamsSchema,
|
|
322
|
+
response: {
|
|
323
|
+
200: AgentActionResultSchema,
|
|
324
|
+
400: ErrorResponseSchema,
|
|
325
|
+
500: ErrorResponseSchema,
|
|
326
|
+
},
|
|
327
|
+
security: [{ cookieAuth: [] }],
|
|
328
|
+
},
|
|
329
|
+
}, async (request, reply) => {
|
|
330
|
+
const { username } = request.params;
|
|
331
|
+
const id = resolveAgentId(username);
|
|
332
|
+
if (!id) {
|
|
333
|
+
return notFound(reply, "Agent not found");
|
|
334
|
+
}
|
|
335
|
+
if (isAgentActive(id)) {
|
|
336
|
+
return badRequest(reply, "Cannot delete an active agent. Stop it first.");
|
|
337
|
+
}
|
|
338
|
+
const agent = await getAgent(id);
|
|
339
|
+
if (!agent) {
|
|
340
|
+
return notFound(reply, "Agent not found");
|
|
341
|
+
}
|
|
342
|
+
if (!agent.archived) {
|
|
343
|
+
return badRequest(reply, "Agent must be archived before it can be deleted.");
|
|
344
|
+
}
|
|
345
|
+
await deleteAgent(id);
|
|
346
|
+
sendUserListChanged();
|
|
347
|
+
return { success: true, message: "Agent permanently deleted" };
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=agentLifecycle.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { AgentUsernameParamsSchema, ArchiveMailResponseSchema, ErrorResponseSchema, MailDataRequestSchema, MailDataResponseSchema, SendMailRequestSchema, SendMailResponseSchema, } from "@naisys/supervisor-shared";
|
|
2
|
+
import { hasPermission, requirePermission } from "../auth-middleware.js";
|
|
3
|
+
import { badRequest, notFound } from "../error-helpers.js";
|
|
4
|
+
import { API_PREFIX } from "../hateoas.js";
|
|
5
|
+
import { resolveAgentId } from "../services/agentService.js";
|
|
6
|
+
import { archiveAllMailMessages, getMailDataByUserId, sendMessage, } from "../services/mailService.js";
|
|
7
|
+
export default function agentMailRoutes(fastify, _options) {
|
|
8
|
+
// GET /:username/mail — Mail for agent
|
|
9
|
+
fastify.get("/:username/mail", {
|
|
10
|
+
schema: {
|
|
11
|
+
description: "Get mail data for a specific agent",
|
|
12
|
+
tags: ["Mail"],
|
|
13
|
+
params: AgentUsernameParamsSchema,
|
|
14
|
+
querystring: MailDataRequestSchema,
|
|
15
|
+
response: {
|
|
16
|
+
200: MailDataResponseSchema,
|
|
17
|
+
500: ErrorResponseSchema,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}, async (request, reply) => {
|
|
21
|
+
const { username } = request.params;
|
|
22
|
+
const { updatedSince, page, count } = request.query;
|
|
23
|
+
const id = resolveAgentId(username);
|
|
24
|
+
if (!id) {
|
|
25
|
+
return notFound(reply, `Agent '${username}' not found`);
|
|
26
|
+
}
|
|
27
|
+
const data = await getMailDataByUserId(id, updatedSince, page, count, "mail");
|
|
28
|
+
const canSend = hasPermission(request.supervisorUser, "agent_communication");
|
|
29
|
+
return {
|
|
30
|
+
success: true,
|
|
31
|
+
message: "Mail data retrieved successfully",
|
|
32
|
+
data,
|
|
33
|
+
_links: data
|
|
34
|
+
? [
|
|
35
|
+
{
|
|
36
|
+
rel: "next",
|
|
37
|
+
href: `${API_PREFIX}/agents/${username}/mail?updatedSince=${encodeURIComponent(data.timestamp)}`,
|
|
38
|
+
title: "Poll for newer mail",
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
: undefined,
|
|
42
|
+
_actions: canSend
|
|
43
|
+
? [
|
|
44
|
+
{
|
|
45
|
+
rel: "send",
|
|
46
|
+
href: `${API_PREFIX}/agents/${username}/mail`,
|
|
47
|
+
method: "POST",
|
|
48
|
+
title: "Send Mail",
|
|
49
|
+
schema: `${API_PREFIX}/schemas/SendMail`,
|
|
50
|
+
body: { fromId: 0, toIds: [0], subject: "", message: "" },
|
|
51
|
+
alternateEncoding: {
|
|
52
|
+
contentType: "multipart/form-data",
|
|
53
|
+
description: "Send as multipart to include file attachments",
|
|
54
|
+
fileFields: ["attachments"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
rel: "archive",
|
|
59
|
+
href: `${API_PREFIX}/agents/${username}/mail/archive`,
|
|
60
|
+
method: "POST",
|
|
61
|
+
title: "Archive All Mail Messages",
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: undefined,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
// POST /:username/mail/archive — Archive all mail messages
|
|
68
|
+
fastify.post("/:username/mail/archive", {
|
|
69
|
+
preHandler: [requirePermission("agent_communication")],
|
|
70
|
+
schema: {
|
|
71
|
+
description: "Archive all mail messages for an agent",
|
|
72
|
+
tags: ["Mail"],
|
|
73
|
+
params: AgentUsernameParamsSchema,
|
|
74
|
+
response: {
|
|
75
|
+
200: ArchiveMailResponseSchema,
|
|
76
|
+
500: ErrorResponseSchema,
|
|
77
|
+
},
|
|
78
|
+
security: [{ cookieAuth: [] }],
|
|
79
|
+
},
|
|
80
|
+
}, async (request, reply) => {
|
|
81
|
+
const { username } = request.params;
|
|
82
|
+
const id = resolveAgentId(username);
|
|
83
|
+
if (!id) {
|
|
84
|
+
return notFound(reply, `Agent '${username}' not found`);
|
|
85
|
+
}
|
|
86
|
+
const archivedCount = await archiveAllMailMessages(id);
|
|
87
|
+
return { success: true, archivedCount };
|
|
88
|
+
});
|
|
89
|
+
// POST /:username/mail — Send mail as agent
|
|
90
|
+
fastify.post("/:username/mail", {
|
|
91
|
+
preHandler: [requirePermission("agent_communication")],
|
|
92
|
+
schema: {
|
|
93
|
+
description: "Send email as agent with optional attachments. Supports JSON and multipart/form-data",
|
|
94
|
+
tags: ["Mail"],
|
|
95
|
+
params: AgentUsernameParamsSchema,
|
|
96
|
+
// No body schema — multipart requests are parsed manually via request.parts()
|
|
97
|
+
response: {
|
|
98
|
+
200: SendMailResponseSchema,
|
|
99
|
+
400: ErrorResponseSchema,
|
|
100
|
+
500: ErrorResponseSchema,
|
|
101
|
+
},
|
|
102
|
+
security: [{ cookieAuth: [] }],
|
|
103
|
+
},
|
|
104
|
+
}, async (request, reply) => {
|
|
105
|
+
const contentType = request.headers["content-type"];
|
|
106
|
+
let fromId = 0, toIds = [], subject = "", message = "";
|
|
107
|
+
let attachments = [];
|
|
108
|
+
if (contentType?.includes("multipart/form-data")) {
|
|
109
|
+
const parts = request.parts();
|
|
110
|
+
for await (const part of parts) {
|
|
111
|
+
if (part.type === "field") {
|
|
112
|
+
const field = part;
|
|
113
|
+
switch (field.fieldname) {
|
|
114
|
+
case "fromId":
|
|
115
|
+
fromId = Number(field.value);
|
|
116
|
+
break;
|
|
117
|
+
case "toIds":
|
|
118
|
+
try {
|
|
119
|
+
toIds = JSON.parse(field.value);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return badRequest(reply, "toIds must be valid JSON array");
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
case "subject":
|
|
126
|
+
subject = field.value;
|
|
127
|
+
break;
|
|
128
|
+
case "message":
|
|
129
|
+
message = field.value;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (part.type === "file") {
|
|
134
|
+
const file = part;
|
|
135
|
+
if (file.fieldname === "attachments") {
|
|
136
|
+
const buffer = await file.toBuffer();
|
|
137
|
+
attachments.push({
|
|
138
|
+
filename: file.filename || "unnamed_file",
|
|
139
|
+
data: buffer,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const body = request.body;
|
|
147
|
+
fromId = body.fromId;
|
|
148
|
+
toIds = body.toIds;
|
|
149
|
+
subject = body.subject;
|
|
150
|
+
message = body.message;
|
|
151
|
+
}
|
|
152
|
+
const parsed = SendMailRequestSchema.safeParse({
|
|
153
|
+
fromId,
|
|
154
|
+
toIds,
|
|
155
|
+
subject,
|
|
156
|
+
message,
|
|
157
|
+
});
|
|
158
|
+
if (!parsed.success) {
|
|
159
|
+
return badRequest(reply, parsed.error.message);
|
|
160
|
+
}
|
|
161
|
+
({ fromId, toIds, subject, message } = parsed.data);
|
|
162
|
+
const result = await sendMessage({ fromId, toIds, subject, message }, attachments.length > 0 ? attachments : undefined);
|
|
163
|
+
if (result.success) {
|
|
164
|
+
return reply.code(200).send(result);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
return reply.code(500).send(result);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=agentMail.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AgentUsernameParamsSchema, ContextLogParamsSchema, ContextLogRequestSchema, ContextLogResponseSchema, RunsDataRequestSchema, RunsDataResponseSchema, } from "@naisys/supervisor-shared";
|
|
2
|
+
import { hasPermission } from "../auth-middleware.js";
|
|
3
|
+
import { notFound } from "../error-helpers.js";
|
|
4
|
+
import { API_PREFIX } from "../hateoas.js";
|
|
5
|
+
import { resolveAgentId } from "../services/agentService.js";
|
|
6
|
+
import { getContextLog, getRunsData, obfuscateLogs, } from "../services/runsService.js";
|
|
7
|
+
export default function agentRunsRoutes(fastify, _options) {
|
|
8
|
+
// GET /:username/runs — Runs for agent
|
|
9
|
+
fastify.get("/:username/runs", {
|
|
10
|
+
schema: {
|
|
11
|
+
description: "Get run sessions for a specific agent",
|
|
12
|
+
tags: ["Runs"],
|
|
13
|
+
params: AgentUsernameParamsSchema,
|
|
14
|
+
querystring: RunsDataRequestSchema,
|
|
15
|
+
response: {
|
|
16
|
+
200: RunsDataResponseSchema,
|
|
17
|
+
500: RunsDataResponseSchema,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}, async (request, reply) => {
|
|
21
|
+
const { username } = request.params;
|
|
22
|
+
const { updatedSince, page, count } = request.query;
|
|
23
|
+
const id = resolveAgentId(username);
|
|
24
|
+
if (!id) {
|
|
25
|
+
return notFound(reply, `Agent '${username}' not found`);
|
|
26
|
+
}
|
|
27
|
+
const data = await getRunsData(id, updatedSince, page, count);
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
message: "Runs data retrieved successfully",
|
|
31
|
+
data: data ?? undefined,
|
|
32
|
+
_linkTemplates: [
|
|
33
|
+
{
|
|
34
|
+
rel: "logs",
|
|
35
|
+
hrefTemplate: `${API_PREFIX}/agents/${username}/runs/{runId}/sessions/{sessionId}/logs`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
_links: data
|
|
39
|
+
? [
|
|
40
|
+
{
|
|
41
|
+
rel: "next",
|
|
42
|
+
href: `${API_PREFIX}/agents/${username}/runs?updatedSince=${encodeURIComponent(data.timestamp)}`,
|
|
43
|
+
title: "Poll for updated runs",
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: undefined,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
// GET /:username/runs/:runId/sessions/:sessionId/logs — Context log
|
|
50
|
+
fastify.get("/:username/runs/:runId/sessions/:sessionId/logs", {
|
|
51
|
+
schema: {
|
|
52
|
+
description: "Get context log for a specific run session",
|
|
53
|
+
tags: ["Runs"],
|
|
54
|
+
params: ContextLogParamsSchema,
|
|
55
|
+
querystring: ContextLogRequestSchema,
|
|
56
|
+
response: {
|
|
57
|
+
200: ContextLogResponseSchema,
|
|
58
|
+
500: ContextLogResponseSchema,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}, async (request, reply) => {
|
|
62
|
+
const { username, runId, sessionId } = request.params;
|
|
63
|
+
const { logsAfter, logsBefore } = request.query;
|
|
64
|
+
const id = resolveAgentId(username);
|
|
65
|
+
if (!id) {
|
|
66
|
+
return notFound(reply, `Agent '${username}' not found`);
|
|
67
|
+
}
|
|
68
|
+
let data = await getContextLog(id, runId, sessionId, logsAfter, logsBefore);
|
|
69
|
+
// Obfuscate log text for users without view_run_logs permission
|
|
70
|
+
if (!hasPermission(request.supervisorUser, "view_run_logs")) {
|
|
71
|
+
data = obfuscateLogs(data);
|
|
72
|
+
}
|
|
73
|
+
const maxLogId = data?.logs.length
|
|
74
|
+
? Math.max(...data.logs.map((l) => l.id))
|
|
75
|
+
: (logsAfter ?? 0);
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
message: "Context log retrieved successfully",
|
|
79
|
+
data,
|
|
80
|
+
_links: [
|
|
81
|
+
{
|
|
82
|
+
rel: "next",
|
|
83
|
+
href: `${API_PREFIX}/agents/${username}/runs/${runId}/sessions/${sessionId}/logs?logsAfter=${maxLogId}`,
|
|
84
|
+
title: "Poll for newer logs",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=agentRuns.js.map
|