@naisys/supervisor 3.0.0-beta.6

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.
Files changed (57) hide show
  1. package/bin/naisys-supervisor +2 -0
  2. package/client-dist/android-chrome-192x192.png +0 -0
  3. package/client-dist/android-chrome-512x512.png +0 -0
  4. package/client-dist/apple-touch-icon.png +0 -0
  5. package/client-dist/assets/index-BBrK4ItN.js +177 -0
  6. package/client-dist/assets/index-CKg0vgt5.css +1 -0
  7. package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
  8. package/client-dist/favicon-16x16.png +0 -0
  9. package/client-dist/favicon-32x32.png +0 -0
  10. package/client-dist/favicon.ico +0 -0
  11. package/client-dist/index.html +49 -0
  12. package/client-dist/site.webmanifest +22 -0
  13. package/dist/api-reference.js +52 -0
  14. package/dist/auth-middleware.js +116 -0
  15. package/dist/database/hubDb.js +26 -0
  16. package/dist/database/supervisorDb.js +18 -0
  17. package/dist/error-helpers.js +13 -0
  18. package/dist/hateoas.js +61 -0
  19. package/dist/logger.js +11 -0
  20. package/dist/route-helpers.js +7 -0
  21. package/dist/routes/admin.js +209 -0
  22. package/dist/routes/agentChat.js +194 -0
  23. package/dist/routes/agentConfig.js +265 -0
  24. package/dist/routes/agentLifecycle.js +350 -0
  25. package/dist/routes/agentMail.js +171 -0
  26. package/dist/routes/agentRuns.js +90 -0
  27. package/dist/routes/agents.js +236 -0
  28. package/dist/routes/api.js +52 -0
  29. package/dist/routes/attachments.js +18 -0
  30. package/dist/routes/auth.js +103 -0
  31. package/dist/routes/costs.js +51 -0
  32. package/dist/routes/hosts.js +296 -0
  33. package/dist/routes/models.js +152 -0
  34. package/dist/routes/root.js +56 -0
  35. package/dist/routes/schemas.js +31 -0
  36. package/dist/routes/status.js +20 -0
  37. package/dist/routes/users.js +420 -0
  38. package/dist/routes/variables.js +103 -0
  39. package/dist/schema-registry.js +23 -0
  40. package/dist/services/agentConfigService.js +182 -0
  41. package/dist/services/agentHostStatusService.js +178 -0
  42. package/dist/services/agentService.js +291 -0
  43. package/dist/services/attachmentProxyService.js +130 -0
  44. package/dist/services/browserSocketService.js +78 -0
  45. package/dist/services/chatService.js +201 -0
  46. package/dist/services/configExportService.js +61 -0
  47. package/dist/services/costsService.js +127 -0
  48. package/dist/services/hostService.js +156 -0
  49. package/dist/services/hubConnectionService.js +333 -0
  50. package/dist/services/logFileService.js +11 -0
  51. package/dist/services/mailService.js +154 -0
  52. package/dist/services/modelService.js +92 -0
  53. package/dist/services/runsService.js +164 -0
  54. package/dist/services/userService.js +147 -0
  55. package/dist/services/variableService.js +23 -0
  56. package/dist/supervisorServer.js +221 -0
  57. package/package.json +79 -0
@@ -0,0 +1,265 @@
1
+ import { AgentConfigFileSchema } from "@naisys/common";
2
+ import { AgentUsernameParamsSchema, ConfigRevisionListResponseSchema, ErrorResponseSchema, ExportAgentConfigResponseSchema, GetAgentConfigResponseSchema, ImportAgentConfigRequestSchema, ImportAgentConfigResponseSchema, UpdateAgentConfigRequestSchema, UpdateAgentConfigResponseSchema, } from "@naisys/supervisor-shared";
3
+ import yaml from "js-yaml";
4
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
5
+ import { badRequest, notFound } from "../error-helpers.js";
6
+ import { API_PREFIX } from "../hateoas.js";
7
+ import { getAgentConfigById, getConfigRevisions, updateAgentConfigById, } from "../services/agentConfigService.js";
8
+ import { resolveAgentId } from "../services/agentService.js";
9
+ import { getAllModelsFromDb } from "../services/modelService.js";
10
+ /** Validate model keys in config against known models. Returns error message or null. */
11
+ async function validateModelKeys(config) {
12
+ const isTemplateVar = (v) => /^\$\{.+\}$/.test(v);
13
+ const allModels = await getAllModelsFromDb();
14
+ const keysOfType = (type) => new Set(allModels
15
+ .filter((r) => r.type === type)
16
+ .map((r) => r.key));
17
+ const validLlmKeys = keysOfType("llm");
18
+ const validImageKeys = keysOfType("image");
19
+ const invalidModels = [];
20
+ if (!isTemplateVar(config.shellModel) &&
21
+ !validLlmKeys.has(config.shellModel)) {
22
+ invalidModels.push(`shellModel: "${config.shellModel}"`);
23
+ }
24
+ if (config.imageModel &&
25
+ !isTemplateVar(config.imageModel) &&
26
+ !validImageKeys.has(config.imageModel)) {
27
+ invalidModels.push(`imageModel: "${config.imageModel}"`);
28
+ }
29
+ if (invalidModels.length > 0) {
30
+ return `Invalid model key(s): ${invalidModels.join(", ")}`;
31
+ }
32
+ return null;
33
+ }
34
+ export default function agentConfigRoutes(fastify, _options) {
35
+ // GET /:username/config — Get parsed agent config
36
+ fastify.get("/:username/config", {
37
+ schema: {
38
+ description: "Get parsed agent configuration",
39
+ tags: ["Agents"],
40
+ params: AgentUsernameParamsSchema,
41
+ response: {
42
+ 200: GetAgentConfigResponseSchema,
43
+ 404: ErrorResponseSchema,
44
+ 500: ErrorResponseSchema,
45
+ },
46
+ },
47
+ }, async (request, reply) => {
48
+ try {
49
+ const { username } = request.params;
50
+ const id = resolveAgentId(username);
51
+ if (!id) {
52
+ return notFound(reply, `Agent '${username}' not found`);
53
+ }
54
+ const config = await getAgentConfigById(id);
55
+ const canManage = hasPermission(request.supervisorUser, "manage_agents");
56
+ return {
57
+ config,
58
+ _actions: canManage
59
+ ? [
60
+ {
61
+ rel: "update",
62
+ href: `${API_PREFIX}/agents/${username}/config`,
63
+ method: "PUT",
64
+ title: "Update Config",
65
+ },
66
+ {
67
+ rel: "import-config",
68
+ href: `${API_PREFIX}/agents/${username}/config/import`,
69
+ method: "POST",
70
+ title: "Import Config",
71
+ },
72
+ {
73
+ rel: "export-config",
74
+ href: `${API_PREFIX}/agents/${username}/config/export`,
75
+ method: "GET",
76
+ title: "Export Config",
77
+ },
78
+ ]
79
+ : undefined,
80
+ };
81
+ }
82
+ catch (error) {
83
+ request.log.error(error, "Error in GET /agents/:username/config route");
84
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
85
+ if (errorMessage.includes("not found")) {
86
+ return notFound(reply, errorMessage);
87
+ }
88
+ throw error;
89
+ }
90
+ });
91
+ // PUT /:username/config — Update agent config
92
+ fastify.put("/:username/config", {
93
+ preHandler: [requirePermission("manage_agents")],
94
+ schema: {
95
+ description: "Update agent configuration",
96
+ tags: ["Agents"],
97
+ params: AgentUsernameParamsSchema,
98
+ body: UpdateAgentConfigRequestSchema,
99
+ response: {
100
+ 200: UpdateAgentConfigResponseSchema,
101
+ 400: ErrorResponseSchema,
102
+ 404: ErrorResponseSchema,
103
+ 500: ErrorResponseSchema,
104
+ },
105
+ security: [{ cookieAuth: [] }],
106
+ },
107
+ }, async (request, reply) => {
108
+ try {
109
+ const { username } = request.params;
110
+ const { config } = request.body;
111
+ const id = resolveAgentId(username);
112
+ if (!id) {
113
+ return notFound(reply, `Agent '${username}' not found`);
114
+ }
115
+ // Validate model keys against known models
116
+ const modelError = await validateModelKeys(config);
117
+ if (modelError) {
118
+ return badRequest(reply, modelError);
119
+ }
120
+ const updatedConfig = await updateAgentConfigById(id, config, true, request.supervisorUser?.id);
121
+ return {
122
+ success: true,
123
+ message: "Agent configuration updated successfully",
124
+ config: updatedConfig,
125
+ };
126
+ }
127
+ catch (error) {
128
+ request.log.error(error, "Error in PUT /agents/:username/config route");
129
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
130
+ if (errorMessage.includes("not found")) {
131
+ return notFound(reply, errorMessage);
132
+ }
133
+ throw error;
134
+ }
135
+ });
136
+ // GET /:username/config/export — Export agent config as YAML
137
+ fastify.get("/:username/config/export", {
138
+ schema: {
139
+ description: "Export agent configuration as YAML",
140
+ tags: ["Agents"],
141
+ params: AgentUsernameParamsSchema,
142
+ response: {
143
+ 200: ExportAgentConfigResponseSchema,
144
+ 404: ErrorResponseSchema,
145
+ 500: ErrorResponseSchema,
146
+ },
147
+ },
148
+ }, async (request, reply) => {
149
+ try {
150
+ const { username } = request.params;
151
+ const id = resolveAgentId(username);
152
+ if (!id) {
153
+ return notFound(reply, `Agent '${username}' not found`);
154
+ }
155
+ const config = await getAgentConfigById(id);
156
+ const yamlString = yaml.dump(config, { lineWidth: -1 });
157
+ return { yaml: yamlString };
158
+ }
159
+ catch (error) {
160
+ request.log.error(error, "Error in GET /agents/:username/config/export route");
161
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
162
+ if (errorMessage.includes("not found")) {
163
+ return notFound(reply, errorMessage);
164
+ }
165
+ throw error;
166
+ }
167
+ });
168
+ // POST /:username/config/import — Import agent config from YAML
169
+ fastify.post("/:username/config/import", {
170
+ preHandler: [requirePermission("manage_agents")],
171
+ schema: {
172
+ description: "Import agent configuration from YAML",
173
+ tags: ["Agents"],
174
+ params: AgentUsernameParamsSchema,
175
+ body: ImportAgentConfigRequestSchema,
176
+ response: {
177
+ 200: ImportAgentConfigResponseSchema,
178
+ 400: ErrorResponseSchema,
179
+ 404: ErrorResponseSchema,
180
+ 500: ErrorResponseSchema,
181
+ },
182
+ security: [{ cookieAuth: [] }],
183
+ },
184
+ }, async (request, reply) => {
185
+ try {
186
+ const { username } = request.params;
187
+ const { yaml: yamlString } = request.body;
188
+ const id = resolveAgentId(username);
189
+ if (!id) {
190
+ return notFound(reply, `Agent '${username}' not found`);
191
+ }
192
+ // Parse YAML
193
+ let parsed;
194
+ try {
195
+ parsed = yaml.load(yamlString);
196
+ }
197
+ catch (err) {
198
+ const message = err instanceof Error ? err.message : "Invalid YAML syntax";
199
+ return badRequest(reply, `YAML parse error: ${message}`);
200
+ }
201
+ // Validate against schema
202
+ let config;
203
+ try {
204
+ config = AgentConfigFileSchema.parse(parsed);
205
+ }
206
+ catch (err) {
207
+ const message = err instanceof Error ? err.message : "Invalid config structure";
208
+ return badRequest(reply, `Config validation error: ${message}`);
209
+ }
210
+ // Validate model keys
211
+ const modelError = await validateModelKeys(config);
212
+ if (modelError) {
213
+ return badRequest(reply, modelError);
214
+ }
215
+ const updatedConfig = await updateAgentConfigById(id, config, false, request.supervisorUser?.id);
216
+ return {
217
+ success: true,
218
+ message: "Agent configuration imported successfully",
219
+ config: updatedConfig,
220
+ };
221
+ }
222
+ catch (error) {
223
+ request.log.error(error, "Error in POST /agents/:username/config/import route");
224
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
225
+ if (errorMessage.includes("not found")) {
226
+ return notFound(reply, errorMessage);
227
+ }
228
+ throw error;
229
+ }
230
+ });
231
+ // GET /:username/config/revisions — List config revision history
232
+ fastify.get("/:username/config/revisions", {
233
+ schema: {
234
+ description: "List config revision history for an agent",
235
+ tags: ["Agents"],
236
+ params: AgentUsernameParamsSchema,
237
+ response: {
238
+ 200: ConfigRevisionListResponseSchema,
239
+ 404: ErrorResponseSchema,
240
+ 500: ErrorResponseSchema,
241
+ },
242
+ },
243
+ }, async (request, reply) => {
244
+ try {
245
+ const { username } = request.params;
246
+ const id = resolveAgentId(username);
247
+ if (!id) {
248
+ return notFound(reply, `Agent '${username}' not found`);
249
+ }
250
+ const revisions = await getConfigRevisions(id);
251
+ return {
252
+ items: revisions.map((r) => ({
253
+ ...r,
254
+ config: yaml.dump(JSON.parse(r.config), { lineWidth: -1 }),
255
+ createdAt: r.createdAt.toISOString(),
256
+ })),
257
+ };
258
+ }
259
+ catch (error) {
260
+ request.log.error(error, "Error in GET /agents/:username/config/revisions route");
261
+ throw error;
262
+ }
263
+ });
264
+ }
265
+ //# sourceMappingURL=agentConfig.js.map
@@ -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