@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.
- 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-BBrK4ItN.js +177 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -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 +52 -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 +130 -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 +333 -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 +164 -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,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
|