@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,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
|
|
@@ -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
|