@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,236 @@
1
+ import { AgentDetailResponseSchema, AgentListRequestSchema, AgentListResponseSchema, AgentUsernameParamsSchema, CreateAgentConfigRequestSchema, CreateAgentConfigResponseSchema, ErrorResponseSchema, } from "@naisys/supervisor-shared";
2
+ import { hasPermission, requirePermission } from "../auth-middleware.js";
3
+ import { badRequest, notFound } from "../error-helpers.js";
4
+ import { API_PREFIX, collectionLink, schemaLink, selfLink, } from "../hateoas.js";
5
+ import { resolveActions } from "../route-helpers.js";
6
+ import { createAgentConfig } from "../services/agentConfigService.js";
7
+ import { getAgentStatus, isAgentActive, } from "../services/agentHostStatusService.js";
8
+ import { getAgent, getAgents, resolveAgentId, } from "../services/agentService.js";
9
+ function agentActions(username, user, enabled, archived, agentId, hasSpendLimit) {
10
+ const active = agentId ? isAgentActive(agentId) : false;
11
+ const href = `${API_PREFIX}/agents/${username}`;
12
+ return resolveActions([
13
+ {
14
+ rel: "start",
15
+ path: "/start",
16
+ method: "POST",
17
+ title: "Start Agent",
18
+ schema: `${API_PREFIX}/schemas/StartAgent`,
19
+ body: { task: "" },
20
+ permission: "manage_agents",
21
+ disabledWhen: (ctx) => ctx.active
22
+ ? "Agent is already running"
23
+ : ctx.archived
24
+ ? "Agent is archived"
25
+ : !ctx.enabled
26
+ ? "Agent is disabled"
27
+ : null,
28
+ },
29
+ {
30
+ rel: "stop",
31
+ path: "/stop",
32
+ method: "POST",
33
+ title: "Stop Agent",
34
+ permission: "manage_agents",
35
+ disabledWhen: (ctx) => (!ctx.active ? "Agent is not running" : null),
36
+ },
37
+ {
38
+ rel: "disable",
39
+ path: "/disable",
40
+ method: "POST",
41
+ title: "Disable Agent",
42
+ permission: "manage_agents",
43
+ visibleWhen: (ctx) => !ctx.archived && ctx.enabled,
44
+ },
45
+ {
46
+ rel: "enable",
47
+ path: "/enable",
48
+ method: "POST",
49
+ title: "Enable Agent",
50
+ permission: "manage_agents",
51
+ visibleWhen: (ctx) => !ctx.archived && !ctx.enabled && !ctx.active,
52
+ },
53
+ {
54
+ rel: "archive",
55
+ path: "/archive",
56
+ method: "POST",
57
+ title: "Archive Agent",
58
+ permission: "manage_agents",
59
+ visibleWhen: (ctx) => !ctx.archived,
60
+ disabledWhen: (ctx) => ctx.active ? "Stop the agent before archiving" : null,
61
+ },
62
+ {
63
+ rel: "unarchive",
64
+ path: "/unarchive",
65
+ method: "POST",
66
+ title: "Unarchive Agent",
67
+ permission: "manage_agents",
68
+ visibleWhen: (ctx) => ctx.archived,
69
+ },
70
+ {
71
+ rel: "delete",
72
+ method: "DELETE",
73
+ title: "Delete Agent",
74
+ permission: "manage_agents",
75
+ visibleWhen: (ctx) => !ctx.active && ctx.archived,
76
+ hideWithoutPermission: true,
77
+ },
78
+ {
79
+ rel: "update-config",
80
+ path: "/config",
81
+ method: "PUT",
82
+ title: "Update Agent Config",
83
+ schema: `${API_PREFIX}/schemas/UpdateAgentConfig`,
84
+ permission: "manage_agents",
85
+ visibleWhen: (ctx) => !ctx.archived,
86
+ },
87
+ {
88
+ rel: "set-lead",
89
+ path: "/lead",
90
+ method: "PUT",
91
+ title: "Set Lead Agent",
92
+ schema: `${API_PREFIX}/schemas/SetLeadAgent`,
93
+ body: { leadAgentUsername: "" },
94
+ permission: "manage_agents",
95
+ visibleWhen: (ctx) => !ctx.archived,
96
+ },
97
+ {
98
+ rel: "reset-spend",
99
+ path: "/reset-spend",
100
+ method: "POST",
101
+ title: "Reset Spend",
102
+ permission: "manage_agents",
103
+ visibleWhen: (ctx) => !ctx.archived && ctx.hasSpendLimit,
104
+ },
105
+ ], href, { user, active, archived, enabled, hasSpendLimit: hasSpendLimit ?? false });
106
+ }
107
+ function agentLinks(username, config) {
108
+ const links = [
109
+ selfLink(`/agents/${username}`),
110
+ { rel: "config", href: `${API_PREFIX}/agents/${username}/config` },
111
+ { rel: "runs", href: `${API_PREFIX}/agents/${username}/runs` },
112
+ collectionLink("agents"),
113
+ ];
114
+ if (config?.mailEnabled) {
115
+ links.push({ rel: "mail", href: `${API_PREFIX}/agents/${username}/mail` });
116
+ }
117
+ if (config?.chatEnabled) {
118
+ links.push({ rel: "chat", href: `${API_PREFIX}/agents/${username}/chat` });
119
+ }
120
+ return links;
121
+ }
122
+ export default function agentsRoutes(fastify, _options) {
123
+ // GET / — List agents
124
+ fastify.get("/", {
125
+ schema: {
126
+ description: "List agents with status and metadata",
127
+ tags: ["Agents"],
128
+ querystring: AgentListRequestSchema,
129
+ response: {
130
+ 200: AgentListResponseSchema,
131
+ 500: ErrorResponseSchema,
132
+ },
133
+ },
134
+ }, async (request, _reply) => {
135
+ const { updatedSince } = request.query;
136
+ const agents = await getAgents(updatedSince);
137
+ const items = agents.map((agent) => ({
138
+ ...agent,
139
+ status: getAgentStatus(agent.id),
140
+ }));
141
+ const hasManagePermission = hasPermission(request.supervisorUser, "manage_agents");
142
+ const actions = [
143
+ {
144
+ rel: "create",
145
+ href: `${API_PREFIX}/agents`,
146
+ method: "POST",
147
+ title: "Create Agent",
148
+ schema: `${API_PREFIX}/schemas/CreateAgent`,
149
+ body: { name: "" },
150
+ ...(hasManagePermission
151
+ ? {}
152
+ : {
153
+ disabled: true,
154
+ disabledReason: "Requires manage_agents permission",
155
+ }),
156
+ },
157
+ ];
158
+ return {
159
+ items,
160
+ timestamp: new Date().toISOString(),
161
+ _links: [selfLink("/agents"), schemaLink("CreateAgent")],
162
+ _linkTemplates: [
163
+ { rel: "item", hrefTemplate: `${API_PREFIX}/agents/{name}` },
164
+ ],
165
+ _actions: actions,
166
+ };
167
+ });
168
+ // POST / — Create agent
169
+ fastify.post("/", {
170
+ preHandler: [requirePermission("manage_agents")],
171
+ schema: {
172
+ description: "Create a new agent with configuration file",
173
+ tags: ["Agents"],
174
+ body: CreateAgentConfigRequestSchema,
175
+ response: {
176
+ 200: CreateAgentConfigResponseSchema,
177
+ 400: ErrorResponseSchema,
178
+ 500: ErrorResponseSchema,
179
+ },
180
+ security: [{ cookieAuth: [] }],
181
+ },
182
+ }, async (request, reply) => {
183
+ try {
184
+ const { name, title } = request.body;
185
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
186
+ return badRequest(reply, "Agent name must contain only alphanumeric characters, hyphens, and underscores");
187
+ }
188
+ const { config } = await createAgentConfig(name, title);
189
+ return {
190
+ success: true,
191
+ message: `Agent '${name}' created successfully`,
192
+ name,
193
+ _links: agentLinks(name, config),
194
+ _actions: agentActions(name, request.supervisorUser, true, false),
195
+ };
196
+ }
197
+ catch (error) {
198
+ request.log.error(error, "Error in POST /agents route");
199
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
200
+ if (errorMessage.includes("already exists")) {
201
+ return badRequest(reply, errorMessage);
202
+ }
203
+ throw error;
204
+ }
205
+ });
206
+ // GET /:username — Agent detail with config
207
+ fastify.get("/:username", {
208
+ schema: {
209
+ description: "Get agent detail with configuration",
210
+ tags: ["Agents"],
211
+ params: AgentUsernameParamsSchema,
212
+ response: {
213
+ 200: AgentDetailResponseSchema,
214
+ 404: ErrorResponseSchema,
215
+ 500: ErrorResponseSchema,
216
+ },
217
+ },
218
+ }, async (request, reply) => {
219
+ const { username } = request.params;
220
+ const id = resolveAgentId(username);
221
+ if (!id) {
222
+ return notFound(reply, `Agent '${username}' not found`);
223
+ }
224
+ const agent = await getAgent(id);
225
+ if (!agent) {
226
+ return notFound(reply, `Agent '${username}' not found`);
227
+ }
228
+ return {
229
+ ...agent,
230
+ status: getAgentStatus(id),
231
+ _links: agentLinks(username, agent.config),
232
+ _actions: agentActions(username, request.supervisorUser, agent.enabled ?? false, agent.archived ?? false, id, agent.config?.spendLimitDollars != null),
233
+ };
234
+ });
235
+ }
236
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1,52 @@
1
+ import { registerAuthMiddleware } from "../auth-middleware.js";
2
+ import adminRoutes from "./admin.js";
3
+ import agentChatRoutes from "./agentChat.js";
4
+ import agentConfigRoutes from "./agentConfig.js";
5
+ import agentLifecycleRoutes from "./agentLifecycle.js";
6
+ import agentMailRoutes from "./agentMail.js";
7
+ import agentRunsRoutes from "./agentRuns.js";
8
+ import agentsRoutes from "./agents.js";
9
+ import attachmentRoutes from "./attachments.js";
10
+ import authRoutes from "./auth.js";
11
+ import costsRoutes from "./costs.js";
12
+ import hostsRoutes from "./hosts.js";
13
+ import modelsRoutes from "./models.js";
14
+ import rootRoutes from "./root.js";
15
+ import schemaRoutes from "./schemas.js";
16
+ import statusRoutes from "./status.js";
17
+ import userRoutes from "./users.js";
18
+ import variablesRoutes from "./variables.js";
19
+ export default async function apiRoutes(fastify, _options) {
20
+ // Register auth middleware for all routes in this scope
21
+ registerAuthMiddleware(fastify);
22
+ // Register root discovery routes
23
+ await fastify.register(rootRoutes);
24
+ // Register auth routes
25
+ await fastify.register(authRoutes);
26
+ // Register schema routes
27
+ await fastify.register(schemaRoutes, { prefix: "/schemas" });
28
+ // Register user routes
29
+ await fastify.register(userRoutes, { prefix: "/users" });
30
+ // Register status routes
31
+ await fastify.register(statusRoutes);
32
+ // Register agents routes
33
+ await fastify.register(agentsRoutes, { prefix: "/agents" });
34
+ await fastify.register(agentLifecycleRoutes, { prefix: "/agents" });
35
+ await fastify.register(agentConfigRoutes, { prefix: "/agents" });
36
+ await fastify.register(agentRunsRoutes, { prefix: "/agents" });
37
+ await fastify.register(agentMailRoutes, { prefix: "/agents" });
38
+ await fastify.register(agentChatRoutes, { prefix: "/agents" });
39
+ // Register hosts routes
40
+ await fastify.register(hostsRoutes, { prefix: "/hosts" });
41
+ // Register models routes
42
+ await fastify.register(modelsRoutes);
43
+ // Register variables routes
44
+ await fastify.register(variablesRoutes, { prefix: "/variables" });
45
+ // Register costs routes
46
+ await fastify.register(costsRoutes, { prefix: "/costs" });
47
+ // Register admin routes
48
+ await fastify.register(adminRoutes, { prefix: "/admin" });
49
+ // Register attachment routes
50
+ await fastify.register(attachmentRoutes, { prefix: "/attachments" });
51
+ }
52
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,18 @@
1
+ import { proxyDownloadFromHub } from "../services/attachmentProxyService.js";
2
+ export default function attachmentRoutes(fastify, _options) {
3
+ // GET /:id or /:id/:filename — Download attachment (proxied through hub)
4
+ const handler = async (request, reply) => {
5
+ const publicId = request.params.id;
6
+ if (!publicId) {
7
+ return reply.code(400).send({ error: "Missing attachment ID" });
8
+ }
9
+ await proxyDownloadFromHub(publicId, reply);
10
+ };
11
+ const schema = {
12
+ description: "Download an attachment by ID (proxied from hub)",
13
+ tags: ["Attachments"],
14
+ };
15
+ fastify.get("/:id", { schema }, handler);
16
+ fastify.get("/:id/:filename", { schema }, handler);
17
+ }
18
+ //# sourceMappingURL=attachments.js.map
@@ -0,0 +1,103 @@
1
+ import { hashToken, SESSION_COOKIE_NAME, sessionCookieOptions, } from "@naisys/common-node";
2
+ import { authenticateAndCreateSession, deleteSession, } from "@naisys/supervisor-database";
3
+ import { AuthUserSchema, ErrorResponseSchema, LoginRequestSchema, LoginResponseSchema, LogoutResponseSchema, } from "@naisys/supervisor-shared";
4
+ import { authCache } from "../auth-middleware.js";
5
+ import { getUserByUsername, getUserPermissions, } from "../services/userService.js";
6
+ let lastLoginRequestTime = 0;
7
+ export default function authRoutes(fastify, _options) {
8
+ const app = fastify.withTypeProvider();
9
+ // LOGIN
10
+ app.post("/auth/login", {
11
+ config: {
12
+ rateLimit: {
13
+ max: 5,
14
+ timeWindow: "1 minute",
15
+ },
16
+ },
17
+ schema: {
18
+ description: "Authenticate with username and password",
19
+ tags: ["Authentication"],
20
+ body: LoginRequestSchema,
21
+ response: {
22
+ 200: LoginResponseSchema,
23
+ 401: ErrorResponseSchema,
24
+ 429: ErrorResponseSchema,
25
+ },
26
+ },
27
+ }, async (request, reply) => {
28
+ const currentTime = Date.now();
29
+ if (currentTime - lastLoginRequestTime < 5000) {
30
+ reply.code(429);
31
+ return {
32
+ success: false,
33
+ message: "Too many requests. Please wait before trying again.",
34
+ };
35
+ }
36
+ lastLoginRequestTime = currentTime;
37
+ const { username, password } = request.body;
38
+ const authResult = await authenticateAndCreateSession(username, password);
39
+ if (!authResult) {
40
+ reply.code(401);
41
+ return {
42
+ success: false,
43
+ message: "Invalid username or password",
44
+ };
45
+ }
46
+ reply.setCookie(SESSION_COOKIE_NAME, authResult.token, sessionCookieOptions(authResult.expiresAt));
47
+ const user = await getUserByUsername(username);
48
+ const permissions = user ? await getUserPermissions(user.id) : [];
49
+ return {
50
+ user: {
51
+ id: user?.id ?? 0,
52
+ username: authResult.user.username,
53
+ permissions,
54
+ },
55
+ };
56
+ });
57
+ // LOGOUT
58
+ app.post("/auth/logout", {
59
+ schema: {
60
+ description: "Log out and clear session",
61
+ tags: ["Authentication"],
62
+ response: {
63
+ 200: LogoutResponseSchema,
64
+ },
65
+ security: [{ cookieAuth: [] }],
66
+ },
67
+ }, async (request, reply) => {
68
+ const token = request.cookies?.[SESSION_COOKIE_NAME];
69
+ // Clear from hub and auth cache
70
+ if (token) {
71
+ const tokenHash = hashToken(token);
72
+ authCache.invalidate(`cookie:${tokenHash}`);
73
+ await deleteSession(tokenHash);
74
+ }
75
+ reply.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
76
+ return {
77
+ success: true,
78
+ message: "Logged out successfully",
79
+ };
80
+ });
81
+ // ME
82
+ app.get("/auth/me", {
83
+ schema: {
84
+ description: "Get current authenticated user",
85
+ tags: ["Authentication"],
86
+ response: {
87
+ 200: AuthUserSchema,
88
+ 401: ErrorResponseSchema,
89
+ },
90
+ security: [{ cookieAuth: [] }],
91
+ },
92
+ }, async (request, reply) => {
93
+ if (!request.supervisorUser) {
94
+ reply.code(401);
95
+ return {
96
+ success: false,
97
+ message: "Not authenticated",
98
+ };
99
+ }
100
+ return request.supervisorUser;
101
+ });
102
+ }
103
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,51 @@
1
+ import { CostsHistogramRequestSchema, CostsHistogramResponseSchema, ErrorResponseSchema, } from "@naisys/supervisor-shared";
2
+ import { findUserIdsForLead, getCostHistogram, getCostsByAgent, getSpendLimitSettings, } from "../services/costsService.js";
3
+ export default function costsRoutes(fastify, _options) {
4
+ fastify.get("/", {
5
+ schema: {
6
+ description: "Get cost histogram data",
7
+ tags: ["Costs"],
8
+ querystring: CostsHistogramRequestSchema,
9
+ response: {
10
+ 200: CostsHistogramResponseSchema,
11
+ 500: ErrorResponseSchema,
12
+ },
13
+ },
14
+ }, async (request, reply) => {
15
+ try {
16
+ const { spendLimitDollars, spendLimitHours } = await getSpendLimitSettings();
17
+ const now = new Date();
18
+ const defaultStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
19
+ const start = request.query.start
20
+ ? new Date(request.query.start)
21
+ : defaultStart;
22
+ const end = request.query.end ? new Date(request.query.end) : now;
23
+ const bucketHours = request.query.bucketHours ?? 24;
24
+ if (isNaN(start.getTime()) ||
25
+ isNaN(end.getTime()) ||
26
+ bucketHours <= 0) {
27
+ return reply
28
+ .code(400)
29
+ .send({ success: false, message: "Invalid query parameters" });
30
+ }
31
+ const userIds = request.query.leadUsername
32
+ ? await findUserIdsForLead(request.query.leadUsername)
33
+ : undefined;
34
+ const [buckets, byAgent] = await Promise.all([
35
+ getCostHistogram(start, end, bucketHours, userIds),
36
+ getCostsByAgent(start, end, userIds),
37
+ ]);
38
+ return {
39
+ spendLimitDollars,
40
+ spendLimitHours,
41
+ buckets,
42
+ byAgent,
43
+ };
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : "Failed to fetch costs";
47
+ return reply.code(500).send({ success: false, message });
48
+ }
49
+ });
50
+ }
51
+ //# sourceMappingURL=costs.js.map