@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.
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-CKg0vgt5.css +1 -0
  6. package/client-dist/assets/index-WzoDF0aQ.js +177 -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 +54 -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 +131 -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 +320 -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 +168 -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,194 @@
1
+ import { AgentUsernameParamsSchema, ArchiveChatResponseSchema, ChatConversationsRequestSchema, ChatConversationsResponseSchema, ChatMessagesRequestSchema, ChatMessagesResponseSchema, ErrorResponseSchema, SendChatRequestSchema, SendChatResponseSchema, } 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 { uploadToHub } from "../services/attachmentProxyService.js";
7
+ import { archiveAllChatMessages, getConversations, getMessages, sendChatMessage, } from "../services/chatService.js";
8
+ function sendChatAction(username) {
9
+ return {
10
+ rel: "send",
11
+ href: `${API_PREFIX}/agents/${username}/chat`,
12
+ method: "POST",
13
+ title: "Send Chat Message",
14
+ schema: `${API_PREFIX}/schemas/SendChat`,
15
+ body: { fromId: 0, toIds: [0], message: "" },
16
+ alternateEncoding: {
17
+ contentType: "multipart/form-data",
18
+ description: "Send as multipart to include file attachments",
19
+ fileFields: ["attachments"],
20
+ },
21
+ };
22
+ }
23
+ function archiveChatAction(username) {
24
+ return {
25
+ rel: "archive",
26
+ href: `${API_PREFIX}/agents/${username}/chat/archive`,
27
+ method: "POST",
28
+ title: "Archive All Chat Messages",
29
+ };
30
+ }
31
+ export default function agentChatRoutes(fastify, _options) {
32
+ // GET /:username/chat — List conversations for agent
33
+ fastify.get("/:username/chat", {
34
+ schema: {
35
+ description: "Get chat conversations for a specific agent",
36
+ tags: ["Chat"],
37
+ params: AgentUsernameParamsSchema,
38
+ querystring: ChatConversationsRequestSchema,
39
+ response: {
40
+ 200: ChatConversationsResponseSchema,
41
+ 500: ErrorResponseSchema,
42
+ },
43
+ },
44
+ }, async (request, reply) => {
45
+ const { username } = request.params;
46
+ const { page, count } = request.query;
47
+ const id = resolveAgentId(username);
48
+ if (!id) {
49
+ return notFound(reply, `Agent '${username}' not found`);
50
+ }
51
+ const { conversations, total } = await getConversations(id, page, count);
52
+ const canSend = hasPermission(request.supervisorUser, "agent_communication");
53
+ return {
54
+ success: true,
55
+ conversations,
56
+ total,
57
+ _actions: canSend
58
+ ? [sendChatAction(username), archiveChatAction(username)]
59
+ : undefined,
60
+ };
61
+ });
62
+ // GET /:username/chat/:participants — Messages in a conversation
63
+ fastify.get("/:username/chat/:participants", {
64
+ schema: {
65
+ description: "Get chat messages for a specific conversation",
66
+ tags: ["Chat"],
67
+ querystring: ChatMessagesRequestSchema,
68
+ response: {
69
+ 200: ChatMessagesResponseSchema,
70
+ 500: ErrorResponseSchema,
71
+ },
72
+ },
73
+ }, async (request, _reply) => {
74
+ const { username, participants } = request.params;
75
+ const { updatedSince, page, count } = request.query;
76
+ const data = await getMessages(participants, updatedSince, page, count);
77
+ const canSend = hasPermission(request.supervisorUser, "agent_communication");
78
+ return {
79
+ success: true,
80
+ messages: data.messages,
81
+ total: data.total,
82
+ timestamp: data.timestamp,
83
+ _actions: canSend ? [sendChatAction(username)] : undefined,
84
+ };
85
+ });
86
+ // POST /:username/chat/archive — Archive all chat messages
87
+ fastify.post("/:username/chat/archive", {
88
+ preHandler: [requirePermission("agent_communication")],
89
+ schema: {
90
+ description: "Archive all chat messages for an agent",
91
+ tags: ["Chat"],
92
+ params: AgentUsernameParamsSchema,
93
+ response: {
94
+ 200: ArchiveChatResponseSchema,
95
+ 500: ErrorResponseSchema,
96
+ },
97
+ security: [{ cookieAuth: [] }],
98
+ },
99
+ }, async (request, reply) => {
100
+ const { username } = request.params;
101
+ const id = resolveAgentId(username);
102
+ if (!id) {
103
+ return notFound(reply, `Agent '${username}' not found`);
104
+ }
105
+ const archivedCount = await archiveAllChatMessages(id);
106
+ return { success: true, archivedCount };
107
+ });
108
+ // POST /:username/chat — Send chat message
109
+ fastify.post("/:username/chat", {
110
+ preHandler: [requirePermission("agent_communication")],
111
+ schema: {
112
+ description: "Send a chat message as an agent with optional attachments. Supports JSON and multipart/form-data",
113
+ tags: ["Chat"],
114
+ params: AgentUsernameParamsSchema,
115
+ // No body schema — multipart requests are parsed manually via request.parts()
116
+ response: {
117
+ 200: SendChatResponseSchema,
118
+ 400: ErrorResponseSchema,
119
+ 500: ErrorResponseSchema,
120
+ },
121
+ security: [{ cookieAuth: [] }],
122
+ },
123
+ }, async (request, reply) => {
124
+ const contentType = request.headers["content-type"];
125
+ let fromId = 0, toIds = [], message = "";
126
+ let attachmentBuffers = [];
127
+ if (contentType?.includes("multipart/form-data")) {
128
+ const parts = request.parts();
129
+ for await (const part of parts) {
130
+ if (part.type === "field") {
131
+ const field = part;
132
+ switch (field.fieldname) {
133
+ case "fromId":
134
+ fromId = Number(field.value);
135
+ break;
136
+ case "toIds":
137
+ try {
138
+ toIds = JSON.parse(field.value);
139
+ }
140
+ catch {
141
+ return badRequest(reply, "toIds must be valid JSON array");
142
+ }
143
+ break;
144
+ case "message":
145
+ message = field.value;
146
+ break;
147
+ }
148
+ }
149
+ else if (part.type === "file") {
150
+ const file = part;
151
+ if (file.fieldname === "attachments") {
152
+ const buffer = await file.toBuffer();
153
+ attachmentBuffers.push({
154
+ filename: file.filename || "unnamed_file",
155
+ data: buffer,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+ else {
162
+ const body = request.body;
163
+ fromId = body.fromId;
164
+ toIds = body.toIds;
165
+ message = body.message;
166
+ }
167
+ const parsed = SendChatRequestSchema.safeParse({
168
+ fromId,
169
+ toIds,
170
+ message,
171
+ });
172
+ if (!parsed.success) {
173
+ return badRequest(reply, parsed.error.message);
174
+ }
175
+ ({ fromId, toIds, message } = parsed.data);
176
+ // Upload attachments to hub and collect IDs
177
+ let attachmentIds;
178
+ if (attachmentBuffers.length > 0) {
179
+ attachmentIds = [];
180
+ for (const att of attachmentBuffers) {
181
+ const id = await uploadToHub(att.data, att.filename, fromId, "mail");
182
+ attachmentIds.push(id);
183
+ }
184
+ }
185
+ const result = await sendChatMessage(fromId, toIds, message, attachmentIds);
186
+ if (result.success) {
187
+ return reply.code(200).send(result);
188
+ }
189
+ else {
190
+ return reply.code(500).send(result);
191
+ }
192
+ });
193
+ }
194
+ //# sourceMappingURL=agentChat.js.map
@@ -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