@dexto/server 1.2.5

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 (174) hide show
  1. package/LICENSE +44 -0
  2. package/dist/a2a/adapters/index.cjs +42 -0
  3. package/dist/a2a/adapters/index.d.ts +10 -0
  4. package/dist/a2a/adapters/index.d.ts.map +1 -0
  5. package/dist/a2a/adapters/index.js +12 -0
  6. package/dist/a2a/adapters/message.cjs +193 -0
  7. package/dist/a2a/adapters/message.d.ts +50 -0
  8. package/dist/a2a/adapters/message.d.ts.map +1 -0
  9. package/dist/a2a/adapters/message.js +167 -0
  10. package/dist/a2a/adapters/state.cjs +57 -0
  11. package/dist/a2a/adapters/state.d.ts +36 -0
  12. package/dist/a2a/adapters/state.d.ts.map +1 -0
  13. package/dist/a2a/adapters/state.js +32 -0
  14. package/dist/a2a/adapters/task-view.cjs +85 -0
  15. package/dist/a2a/adapters/task-view.d.ts +58 -0
  16. package/dist/a2a/adapters/task-view.d.ts.map +1 -0
  17. package/dist/a2a/adapters/task-view.js +60 -0
  18. package/dist/a2a/index.cjs +51 -0
  19. package/dist/a2a/index.d.ts +15 -0
  20. package/dist/a2a/index.d.ts.map +1 -0
  21. package/dist/a2a/index.js +30 -0
  22. package/dist/a2a/jsonrpc/index.cjs +38 -0
  23. package/dist/a2a/jsonrpc/index.d.ts +11 -0
  24. package/dist/a2a/jsonrpc/index.d.ts.map +1 -0
  25. package/dist/a2a/jsonrpc/index.js +10 -0
  26. package/dist/a2a/jsonrpc/methods.cjs +183 -0
  27. package/dist/a2a/jsonrpc/methods.d.ts +110 -0
  28. package/dist/a2a/jsonrpc/methods.d.ts.map +1 -0
  29. package/dist/a2a/jsonrpc/methods.js +159 -0
  30. package/dist/a2a/jsonrpc/server.cjs +199 -0
  31. package/dist/a2a/jsonrpc/server.d.ts +100 -0
  32. package/dist/a2a/jsonrpc/server.d.ts.map +1 -0
  33. package/dist/a2a/jsonrpc/server.js +175 -0
  34. package/dist/a2a/jsonrpc/types.cjs +47 -0
  35. package/dist/a2a/jsonrpc/types.d.ts +91 -0
  36. package/dist/a2a/jsonrpc/types.d.ts.map +1 -0
  37. package/dist/a2a/jsonrpc/types.js +21 -0
  38. package/dist/a2a/types.cjs +16 -0
  39. package/dist/a2a/types.d.ts +250 -0
  40. package/dist/a2a/types.d.ts.map +1 -0
  41. package/dist/a2a/types.js +0 -0
  42. package/dist/approval/approval-coordinator.cjs +87 -0
  43. package/dist/approval/approval-coordinator.d.ts +52 -0
  44. package/dist/approval/approval-coordinator.d.ts.map +1 -0
  45. package/dist/approval/approval-coordinator.js +63 -0
  46. package/dist/approval/manual-approval-handler.cjs +100 -0
  47. package/dist/approval/manual-approval-handler.d.ts +32 -0
  48. package/dist/approval/manual-approval-handler.d.ts.map +1 -0
  49. package/dist/approval/manual-approval-handler.js +76 -0
  50. package/dist/events/a2a-sse-subscriber.cjs +271 -0
  51. package/dist/events/a2a-sse-subscriber.d.ts +94 -0
  52. package/dist/events/a2a-sse-subscriber.d.ts.map +1 -0
  53. package/dist/events/a2a-sse-subscriber.js +247 -0
  54. package/dist/events/types.cjs +16 -0
  55. package/dist/events/types.d.ts +15 -0
  56. package/dist/events/types.d.ts.map +1 -0
  57. package/dist/events/types.js +0 -0
  58. package/dist/events/webhook-subscriber.cjs +301 -0
  59. package/dist/events/webhook-subscriber.d.ts +64 -0
  60. package/dist/events/webhook-subscriber.d.ts.map +1 -0
  61. package/dist/events/webhook-subscriber.js +269 -0
  62. package/dist/events/webhook-types.cjs +16 -0
  63. package/dist/events/webhook-types.d.ts +91 -0
  64. package/dist/events/webhook-types.d.ts.map +1 -0
  65. package/dist/events/webhook-types.js +0 -0
  66. package/dist/hono/__tests__/test-fixtures.cjs +236 -0
  67. package/dist/hono/__tests__/test-fixtures.d.ts +65 -0
  68. package/dist/hono/__tests__/test-fixtures.d.ts.map +1 -0
  69. package/dist/hono/__tests__/test-fixtures.js +197 -0
  70. package/dist/hono/index.cjs +166 -0
  71. package/dist/hono/index.d.ts +2783 -0
  72. package/dist/hono/index.d.ts.map +1 -0
  73. package/dist/hono/index.js +141 -0
  74. package/dist/hono/middleware/auth.cjs +75 -0
  75. package/dist/hono/middleware/auth.d.ts +3 -0
  76. package/dist/hono/middleware/auth.d.ts.map +1 -0
  77. package/dist/hono/middleware/auth.js +51 -0
  78. package/dist/hono/middleware/cors.cjs +57 -0
  79. package/dist/hono/middleware/cors.d.ts +9 -0
  80. package/dist/hono/middleware/cors.d.ts.map +1 -0
  81. package/dist/hono/middleware/cors.js +33 -0
  82. package/dist/hono/middleware/error.cjs +131 -0
  83. package/dist/hono/middleware/error.d.ts +5 -0
  84. package/dist/hono/middleware/error.d.ts.map +1 -0
  85. package/dist/hono/middleware/error.js +105 -0
  86. package/dist/hono/middleware/redaction.cjs +45 -0
  87. package/dist/hono/middleware/redaction.d.ts +4 -0
  88. package/dist/hono/middleware/redaction.d.ts.map +1 -0
  89. package/dist/hono/middleware/redaction.js +20 -0
  90. package/dist/hono/node/index.cjs +139 -0
  91. package/dist/hono/node/index.d.ts +19 -0
  92. package/dist/hono/node/index.d.ts.map +1 -0
  93. package/dist/hono/node/index.js +115 -0
  94. package/dist/hono/routes/a2a-jsonrpc.cjs +119 -0
  95. package/dist/hono/routes/a2a-jsonrpc.d.ts +46 -0
  96. package/dist/hono/routes/a2a-jsonrpc.d.ts.map +1 -0
  97. package/dist/hono/routes/a2a-jsonrpc.js +95 -0
  98. package/dist/hono/routes/a2a-tasks.cjs +315 -0
  99. package/dist/hono/routes/a2a-tasks.d.ts +530 -0
  100. package/dist/hono/routes/a2a-tasks.d.ts.map +1 -0
  101. package/dist/hono/routes/a2a-tasks.js +291 -0
  102. package/dist/hono/routes/a2a.cjs +36 -0
  103. package/dist/hono/routes/a2a.d.ts +4 -0
  104. package/dist/hono/routes/a2a.d.ts.map +1 -0
  105. package/dist/hono/routes/a2a.js +12 -0
  106. package/dist/hono/routes/agents.cjs +735 -0
  107. package/dist/hono/routes/agents.d.ts +650 -0
  108. package/dist/hono/routes/agents.d.ts.map +1 -0
  109. package/dist/hono/routes/agents.js +711 -0
  110. package/dist/hono/routes/approvals.cjs +125 -0
  111. package/dist/hono/routes/approvals.d.ts +89 -0
  112. package/dist/hono/routes/approvals.d.ts.map +1 -0
  113. package/dist/hono/routes/approvals.js +101 -0
  114. package/dist/hono/routes/greeting.cjs +60 -0
  115. package/dist/hono/routes/greeting.d.ts +19 -0
  116. package/dist/hono/routes/greeting.d.ts.map +1 -0
  117. package/dist/hono/routes/greeting.js +36 -0
  118. package/dist/hono/routes/health.cjs +45 -0
  119. package/dist/hono/routes/health.d.ts +17 -0
  120. package/dist/hono/routes/health.d.ts.map +1 -0
  121. package/dist/hono/routes/health.js +21 -0
  122. package/dist/hono/routes/llm.cjs +298 -0
  123. package/dist/hono/routes/llm.d.ts +294 -0
  124. package/dist/hono/routes/llm.d.ts.map +1 -0
  125. package/dist/hono/routes/llm.js +287 -0
  126. package/dist/hono/routes/mcp.cjs +356 -0
  127. package/dist/hono/routes/mcp.d.ts +246 -0
  128. package/dist/hono/routes/mcp.d.ts.map +1 -0
  129. package/dist/hono/routes/mcp.js +332 -0
  130. package/dist/hono/routes/memory.cjs +192 -0
  131. package/dist/hono/routes/memory.d.ts +146 -0
  132. package/dist/hono/routes/memory.d.ts.map +1 -0
  133. package/dist/hono/routes/memory.js +168 -0
  134. package/dist/hono/routes/messages.cjs +320 -0
  135. package/dist/hono/routes/messages.d.ts +163 -0
  136. package/dist/hono/routes/messages.d.ts.map +1 -0
  137. package/dist/hono/routes/messages.js +296 -0
  138. package/dist/hono/routes/prompts.cjs +228 -0
  139. package/dist/hono/routes/prompts.d.ts +150 -0
  140. package/dist/hono/routes/prompts.d.ts.map +1 -0
  141. package/dist/hono/routes/prompts.js +204 -0
  142. package/dist/hono/routes/resources.cjs +110 -0
  143. package/dist/hono/routes/resources.d.ts +76 -0
  144. package/dist/hono/routes/resources.d.ts.map +1 -0
  145. package/dist/hono/routes/resources.js +86 -0
  146. package/dist/hono/routes/search.cjs +109 -0
  147. package/dist/hono/routes/search.d.ts +137 -0
  148. package/dist/hono/routes/search.d.ts.map +1 -0
  149. package/dist/hono/routes/search.js +85 -0
  150. package/dist/hono/routes/sessions.cjs +366 -0
  151. package/dist/hono/routes/sessions.d.ts +229 -0
  152. package/dist/hono/routes/sessions.d.ts.map +1 -0
  153. package/dist/hono/routes/sessions.js +342 -0
  154. package/dist/hono/routes/webhooks.cjs +228 -0
  155. package/dist/hono/routes/webhooks.d.ts +127 -0
  156. package/dist/hono/routes/webhooks.d.ts.map +1 -0
  157. package/dist/hono/routes/webhooks.js +204 -0
  158. package/dist/hono/schemas/responses.cjs +276 -0
  159. package/dist/hono/schemas/responses.d.ts +1418 -0
  160. package/dist/hono/schemas/responses.d.ts.map +1 -0
  161. package/dist/hono/schemas/responses.js +227 -0
  162. package/dist/hono/types.cjs +16 -0
  163. package/dist/hono/types.d.ts +6 -0
  164. package/dist/hono/types.d.ts.map +1 -0
  165. package/dist/hono/types.js +0 -0
  166. package/dist/index.cjs +38 -0
  167. package/dist/index.d.ts +11 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +9 -0
  170. package/dist/mcp/mcp-handler.cjs +145 -0
  171. package/dist/mcp/mcp-handler.d.ts +14 -0
  172. package/dist/mcp/mcp-handler.d.ts.map +1 -0
  173. package/dist/mcp/mcp-handler.js +118 -0
  174. package/package.json +59 -0
@@ -0,0 +1,711 @@
1
+ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
2
+ import {
3
+ logger,
4
+ safeStringify,
5
+ AgentConfigSchema,
6
+ zodToIssues
7
+ } from "@dexto/core";
8
+ import {
9
+ getPrimaryApiKeyEnvVar,
10
+ saveProviderApiKey,
11
+ reloadAgentConfigFromFile,
12
+ enrichAgentConfig
13
+ } from "@dexto/agent-management";
14
+ import { Dexto, deriveDisplayName } from "@dexto/agent-management";
15
+ import { stringify as yamlStringify, parse as yamlParse } from "yaml";
16
+ import os from "os";
17
+ import path from "path";
18
+ import { promises as fs } from "fs";
19
+ import { DextoValidationError, AgentErrorCode, ErrorScope, ErrorType } from "@dexto/core";
20
+ import { AgentRegistryEntrySchema } from "../schemas/responses.js";
21
+ const AgentIdentifierSchema = z.object({
22
+ id: z.string().min(1, "Agent id is required").describe('Unique agent identifier (e.g., "database-agent")'),
23
+ path: z.string().optional().describe(
24
+ 'Optional absolute file path for file-based agents (e.g., "/path/to/agent.yml")'
25
+ )
26
+ }).strict().describe("Agent identifier for switching agents by ID or file path");
27
+ const UninstallAgentSchema = z.object({
28
+ id: z.string().min(1, "Agent id is required").describe("Unique agent identifier to uninstall"),
29
+ force: z.boolean().default(false).describe("Force uninstall even if agent is currently active")
30
+ }).strict().describe("Request body for uninstalling an agent");
31
+ const CustomAgentInstallSchema = z.object({
32
+ id: z.string().min(1, "Agent id is required").describe("Unique agent identifier"),
33
+ name: z.string().optional().describe("Display name (defaults to derived from id)"),
34
+ sourcePath: z.string().min(1).describe("Path to agent configuration file or directory"),
35
+ metadata: z.object({
36
+ description: z.string().min(1).describe("Human-readable description of the agent"),
37
+ author: z.string().min(1).describe("Agent author or organization name"),
38
+ tags: z.array(z.string()).describe("Tags for categorizing the agent"),
39
+ main: z.string().optional().describe("Main configuration file name within source directory")
40
+ }).strict().describe("Agent metadata including description, author, and tags"),
41
+ injectPreferences: z.boolean().default(true).describe("Whether to inject user preferences into agent config")
42
+ }).strict().describe("Request body for installing a custom agent from file system").transform((value) => {
43
+ const displayName = value.name?.trim() || deriveDisplayName(value.id);
44
+ return {
45
+ id: value.id,
46
+ displayName,
47
+ sourcePath: value.sourcePath,
48
+ metadata: value.metadata,
49
+ injectPreferences: value.injectPreferences
50
+ };
51
+ });
52
+ const CustomAgentCreateSchema = z.object({
53
+ // Registry metadata
54
+ id: z.string().min(1, "Agent ID is required").regex(
55
+ /^[a-z0-9-]+$/,
56
+ "Agent ID must contain only lowercase letters, numbers, and hyphens"
57
+ ).describe("Unique agent identifier"),
58
+ name: z.string().min(1, "Agent name is required").describe("Display name for the agent"),
59
+ description: z.string().min(1, "Description is required").describe("One-line description of the agent"),
60
+ author: z.string().optional().describe("Author or organization"),
61
+ tags: z.array(z.string()).default([]).describe("Tags for discovery"),
62
+ // Full agent configuration
63
+ config: AgentConfigSchema.describe("Complete agent configuration")
64
+ }).strict().describe("Request body for creating a new custom agent with full configuration");
65
+ const AgentConfigValidateSchema = z.object({
66
+ yaml: z.string().describe("YAML agent configuration content to validate")
67
+ }).describe("Request body for validating agent configuration YAML");
68
+ const AgentConfigSaveSchema = z.object({
69
+ yaml: z.string().min(1, "YAML content is required").describe("YAML agent configuration content to save")
70
+ }).describe("Request body for saving agent configuration YAML");
71
+ const AgentInfoNullableSchema = z.object({
72
+ id: z.string().nullable().describe("Agent identifier (null if no active agent)"),
73
+ name: z.string().nullable().describe("Agent display name (null if no active agent)")
74
+ }).strict().describe("Basic agent information (nullable)");
75
+ const ListAgentsResponseSchema = z.object({
76
+ installed: z.array(AgentRegistryEntrySchema).describe("Agents installed locally"),
77
+ available: z.array(AgentRegistryEntrySchema).describe("Agents available from registry"),
78
+ current: AgentInfoNullableSchema.describe("Currently active agent")
79
+ }).strict().describe("List of all agents");
80
+ const InstallAgentResponseSchema = z.object({
81
+ installed: z.literal(true).describe("Indicates successful installation"),
82
+ id: z.string().describe("Installed agent ID"),
83
+ name: z.string().describe("Installed agent name"),
84
+ type: z.enum(["builtin", "custom"]).describe("Type of agent installed")
85
+ }).strict().describe("Agent installation response");
86
+ const SwitchAgentResponseSchema = z.object({
87
+ switched: z.literal(true).describe("Indicates successful agent switch"),
88
+ id: z.string().describe("New active agent ID"),
89
+ name: z.string().describe("New active agent name")
90
+ }).strict().describe("Agent switch response");
91
+ const ValidateNameResponseSchema = z.object({
92
+ valid: z.boolean().describe("Whether the agent name is valid"),
93
+ conflict: z.string().optional().describe("Type of conflict if name is invalid"),
94
+ message: z.string().optional().describe("Validation message")
95
+ }).strict().describe("Agent name validation result");
96
+ const UninstallAgentResponseSchema = z.object({
97
+ uninstalled: z.literal(true).describe("Indicates successful uninstallation"),
98
+ id: z.string().describe("Uninstalled agent ID")
99
+ }).strict().describe("Agent uninstallation response");
100
+ const AgentPathResponseSchema = z.object({
101
+ path: z.string().describe("Absolute path to agent configuration file"),
102
+ relativePath: z.string().describe("Relative path or basename"),
103
+ name: z.string().describe("Agent configuration filename without extension"),
104
+ isDefault: z.boolean().describe("Whether this is the default agent")
105
+ }).strict().describe("Agent file path information");
106
+ const AgentConfigResponseSchema = z.object({
107
+ yaml: z.string().describe("Raw YAML configuration content"),
108
+ path: z.string().describe("Absolute path to configuration file"),
109
+ relativePath: z.string().describe("Relative path or basename"),
110
+ lastModified: z.date().describe("Last modification timestamp"),
111
+ warnings: z.array(z.string()).describe("Configuration warnings")
112
+ }).strict().describe("Agent configuration content");
113
+ const SaveConfigResponseSchema = z.object({
114
+ ok: z.literal(true).describe("Indicates successful save"),
115
+ path: z.string().describe("Path to saved configuration file"),
116
+ reloaded: z.boolean().describe("Whether configuration was reloaded"),
117
+ restarted: z.boolean().describe("Whether agent was restarted"),
118
+ changesApplied: z.array(z.string()).describe("List of changes that were applied"),
119
+ message: z.string().describe("Success message")
120
+ }).strict().describe("Configuration save result");
121
+ function createAgentsRouter(getAgent, context) {
122
+ const app = new OpenAPIHono();
123
+ const { switchAgentById, switchAgentByPath, resolveAgentInfo, getActiveAgentId } = context;
124
+ const listRoute = createRoute({
125
+ method: "get",
126
+ path: "/agents",
127
+ summary: "List Agents",
128
+ description: "Retrieves all agents (installed, available, and current active agent)",
129
+ tags: ["agents"],
130
+ responses: {
131
+ 200: {
132
+ description: "List all agents",
133
+ content: { "application/json": { schema: ListAgentsResponseSchema } }
134
+ }
135
+ }
136
+ });
137
+ const currentRoute = createRoute({
138
+ method: "get",
139
+ path: "/agents/current",
140
+ summary: "Get Current Agent",
141
+ description: "Retrieves the currently active agent",
142
+ tags: ["agents"],
143
+ responses: {
144
+ 200: {
145
+ description: "Current agent",
146
+ content: { "application/json": { schema: AgentInfoNullableSchema } }
147
+ }
148
+ }
149
+ });
150
+ const installRoute = createRoute({
151
+ method: "post",
152
+ path: "/agents/install",
153
+ summary: "Install Agent",
154
+ description: "Installs an agent from the registry or from a custom source",
155
+ tags: ["agents"],
156
+ request: {
157
+ body: {
158
+ content: {
159
+ "application/json": {
160
+ schema: z.union([CustomAgentInstallSchema, AgentIdentifierSchema])
161
+ }
162
+ }
163
+ }
164
+ },
165
+ responses: {
166
+ 201: {
167
+ description: "Agent installed",
168
+ content: { "application/json": { schema: InstallAgentResponseSchema } }
169
+ }
170
+ }
171
+ });
172
+ const switchRoute = createRoute({
173
+ method: "post",
174
+ path: "/agents/switch",
175
+ summary: "Switch Agent",
176
+ description: "Switches to a different agent by ID or file path",
177
+ tags: ["agents"],
178
+ request: {
179
+ body: {
180
+ content: {
181
+ "application/json": {
182
+ schema: AgentIdentifierSchema
183
+ }
184
+ }
185
+ }
186
+ },
187
+ responses: {
188
+ 200: {
189
+ description: "Agent switched",
190
+ content: { "application/json": { schema: SwitchAgentResponseSchema } }
191
+ }
192
+ }
193
+ });
194
+ const validateNameRoute = createRoute({
195
+ method: "post",
196
+ path: "/agents/validate-name",
197
+ summary: "Validate Agent Name",
198
+ description: "Checks if an agent ID conflicts with existing agents",
199
+ tags: ["agents"],
200
+ request: {
201
+ body: {
202
+ content: {
203
+ "application/json": {
204
+ schema: AgentIdentifierSchema
205
+ }
206
+ }
207
+ }
208
+ },
209
+ responses: {
210
+ 200: {
211
+ description: "Name validation result",
212
+ content: { "application/json": { schema: ValidateNameResponseSchema } }
213
+ }
214
+ }
215
+ });
216
+ const uninstallRoute = createRoute({
217
+ method: "post",
218
+ path: "/agents/uninstall",
219
+ summary: "Uninstall Agent",
220
+ description: "Removes an agent from the system. Custom agents are removed from registry; builtin agents can be reinstalled",
221
+ tags: ["agents"],
222
+ request: {
223
+ body: {
224
+ content: {
225
+ "application/json": {
226
+ schema: UninstallAgentSchema
227
+ }
228
+ }
229
+ }
230
+ },
231
+ responses: {
232
+ 200: {
233
+ description: "Agent uninstalled",
234
+ content: { "application/json": { schema: UninstallAgentResponseSchema } }
235
+ }
236
+ }
237
+ });
238
+ const customCreateRoute = createRoute({
239
+ method: "post",
240
+ path: "/agents/custom/create",
241
+ summary: "Create Custom Agent",
242
+ description: "Creates a new custom agent from scratch via the UI/API",
243
+ tags: ["agents"],
244
+ request: {
245
+ body: {
246
+ content: {
247
+ "application/json": {
248
+ schema: CustomAgentCreateSchema
249
+ }
250
+ }
251
+ }
252
+ },
253
+ responses: {
254
+ 201: {
255
+ description: "Custom agent created",
256
+ content: {
257
+ "application/json": {
258
+ schema: z.object({
259
+ created: z.literal(true).describe("Creation success indicator"),
260
+ id: z.string().describe("Agent identifier"),
261
+ name: z.string().describe("Agent name")
262
+ }).strict()
263
+ }
264
+ }
265
+ }
266
+ }
267
+ });
268
+ const getPathRoute = createRoute({
269
+ method: "get",
270
+ path: "/agent/path",
271
+ summary: "Get Agent File Path",
272
+ description: "Retrieves the file path of the currently active agent configuration",
273
+ tags: ["agent"],
274
+ responses: {
275
+ 200: {
276
+ description: "Agent file path",
277
+ content: {
278
+ "application/json": {
279
+ schema: AgentPathResponseSchema
280
+ }
281
+ }
282
+ }
283
+ }
284
+ });
285
+ const getConfigRoute = createRoute({
286
+ method: "get",
287
+ path: "/agent/config",
288
+ summary: "Get Agent Configuration",
289
+ description: "Retrieves the raw YAML configuration of the currently active agent",
290
+ tags: ["agent"],
291
+ responses: {
292
+ 200: {
293
+ description: "Agent configuration",
294
+ content: {
295
+ "application/json": {
296
+ schema: AgentConfigResponseSchema
297
+ }
298
+ }
299
+ }
300
+ }
301
+ });
302
+ const validateConfigRoute = createRoute({
303
+ method: "post",
304
+ path: "/agent/validate",
305
+ summary: "Validate Agent Configuration",
306
+ description: "Validates YAML agent configuration without saving it",
307
+ tags: ["agent"],
308
+ request: {
309
+ body: {
310
+ content: {
311
+ "application/json": {
312
+ schema: AgentConfigValidateSchema
313
+ }
314
+ }
315
+ }
316
+ },
317
+ responses: {
318
+ 200: {
319
+ description: "Validation result",
320
+ content: {
321
+ "application/json": {
322
+ schema: z.object({
323
+ valid: z.boolean().describe("Whether configuration is valid"),
324
+ errors: z.array(
325
+ z.object({
326
+ line: z.number().int().optional().describe("Line number"),
327
+ column: z.number().int().optional().describe("Column number"),
328
+ path: z.string().optional().describe("Configuration path"),
329
+ message: z.string().describe("Error message"),
330
+ code: z.string().describe("Error code")
331
+ }).passthrough()
332
+ ).describe("Validation errors"),
333
+ warnings: z.array(
334
+ z.object({
335
+ path: z.string().describe("Configuration path"),
336
+ message: z.string().describe("Warning message"),
337
+ code: z.string().describe("Warning code")
338
+ }).strict()
339
+ ).describe("Configuration warnings")
340
+ }).strict()
341
+ }
342
+ }
343
+ }
344
+ }
345
+ });
346
+ const saveConfigRoute = createRoute({
347
+ method: "post",
348
+ path: "/agent/config",
349
+ summary: "Save Agent Configuration",
350
+ description: "Saves and applies YAML agent configuration. Creates backup before saving",
351
+ tags: ["agent"],
352
+ request: {
353
+ body: {
354
+ content: {
355
+ "application/json": {
356
+ schema: AgentConfigSaveSchema
357
+ }
358
+ }
359
+ }
360
+ },
361
+ responses: {
362
+ 200: {
363
+ description: "Configuration saved",
364
+ content: {
365
+ "application/json": {
366
+ schema: SaveConfigResponseSchema
367
+ }
368
+ }
369
+ }
370
+ }
371
+ });
372
+ const exportConfigRoute = createRoute({
373
+ method: "get",
374
+ path: "/agent/config/export",
375
+ summary: "Export Agent Configuration",
376
+ description: "Exports the effective agent configuration with sensitive values redacted",
377
+ tags: ["agent"],
378
+ request: {
379
+ query: z.object({
380
+ sessionId: z.string().optional().describe("Session identifier to export session-specific configuration")
381
+ })
382
+ },
383
+ responses: {
384
+ 200: {
385
+ description: "Exported configuration",
386
+ content: { "application/x-yaml": { schema: z.string() } }
387
+ }
388
+ }
389
+ });
390
+ return app.openapi(listRoute, async (ctx) => {
391
+ const agents = await Dexto.listAgents();
392
+ const currentId = getActiveAgentId() ?? null;
393
+ return ctx.json({
394
+ installed: agents.installed,
395
+ available: agents.available,
396
+ current: currentId ? await resolveAgentInfo(currentId) : { id: null, name: null }
397
+ });
398
+ }).openapi(currentRoute, async (ctx) => {
399
+ const currentId = getActiveAgentId() ?? null;
400
+ if (!currentId) {
401
+ return ctx.json({ id: null, name: null });
402
+ }
403
+ return ctx.json(await resolveAgentInfo(currentId));
404
+ }).openapi(installRoute, async (ctx) => {
405
+ const body = ctx.req.valid("json");
406
+ if ("sourcePath" in body && "metadata" in body) {
407
+ const { id, displayName, sourcePath, metadata, injectPreferences } = body;
408
+ await Dexto.installCustomAgent(
409
+ id,
410
+ sourcePath,
411
+ {
412
+ name: displayName,
413
+ description: metadata.description,
414
+ author: metadata.author,
415
+ tags: metadata.tags,
416
+ ...metadata.main ? { main: metadata.main } : {}
417
+ },
418
+ injectPreferences
419
+ );
420
+ return ctx.json(
421
+ { installed: true, id, name: displayName, type: "custom" },
422
+ 201
423
+ );
424
+ } else {
425
+ const { id } = body;
426
+ await Dexto.installAgent(id);
427
+ const agentInfo = await resolveAgentInfo(id);
428
+ return ctx.json(
429
+ {
430
+ installed: true,
431
+ ...agentInfo,
432
+ type: "builtin"
433
+ },
434
+ 201
435
+ );
436
+ }
437
+ }).openapi(switchRoute, async (ctx) => {
438
+ const { id, path: filePath } = ctx.req.valid("json");
439
+ const result = filePath ? await switchAgentByPath(filePath) : await switchAgentById(id);
440
+ return ctx.json({ switched: true, ...result });
441
+ }).openapi(validateNameRoute, async (ctx) => {
442
+ const { id } = ctx.req.valid("json");
443
+ const agents = await Dexto.listAgents();
444
+ const installedAgent = agents.installed.find((a) => a.id === id);
445
+ if (installedAgent) {
446
+ return ctx.json({
447
+ valid: false,
448
+ conflict: installedAgent.type,
449
+ message: `Agent id '${id}' already exists (${installedAgent.type})`
450
+ });
451
+ }
452
+ const availableAgent = agents.available.find((a) => a.id === id);
453
+ if (availableAgent) {
454
+ return ctx.json({
455
+ valid: false,
456
+ conflict: availableAgent.type,
457
+ message: `Agent id '${id}' conflicts with ${availableAgent.type} agent`
458
+ });
459
+ }
460
+ return ctx.json({ valid: true });
461
+ }).openapi(uninstallRoute, async (ctx) => {
462
+ const { id, force } = ctx.req.valid("json");
463
+ await Dexto.uninstallAgent(id, force);
464
+ return ctx.json({ uninstalled: true, id });
465
+ }).openapi(customCreateRoute, async (ctx) => {
466
+ const { id, name, description, author, tags, config } = ctx.req.valid("json");
467
+ const provider = config.llm.provider;
468
+ let agentConfig = config;
469
+ if (config.llm.apiKey && !config.llm.apiKey.startsWith("$")) {
470
+ const meta = await saveProviderApiKey(provider, config.llm.apiKey, process.cwd());
471
+ const apiKeyRef = `$${meta.envVar}`;
472
+ logger.info(
473
+ `Stored API key securely for ${provider}, using env var: ${meta.envVar}`
474
+ );
475
+ agentConfig = {
476
+ ...config,
477
+ llm: {
478
+ ...config.llm,
479
+ apiKey: apiKeyRef
480
+ }
481
+ };
482
+ } else if (!config.llm.apiKey) {
483
+ agentConfig = {
484
+ ...config,
485
+ llm: {
486
+ ...config.llm,
487
+ apiKey: `$${getPrimaryApiKeyEnvVar(provider)}`
488
+ }
489
+ };
490
+ }
491
+ const yamlContent = yamlStringify(agentConfig);
492
+ logger.info(
493
+ `Creating agent config for ${id}: agentConfig=${safeStringify(agentConfig)}, yamlContent=${yamlContent}`
494
+ );
495
+ const tmpDir = os.tmpdir();
496
+ const tmpFile = path.join(tmpDir, `${id}-${Date.now()}.yml`);
497
+ await fs.writeFile(tmpFile, yamlContent, "utf-8");
498
+ try {
499
+ await Dexto.installCustomAgent(
500
+ id,
501
+ tmpFile,
502
+ {
503
+ name,
504
+ description,
505
+ author: author || "Custom",
506
+ tags: tags || []
507
+ },
508
+ false
509
+ // Don't inject preferences
510
+ );
511
+ await fs.unlink(tmpFile).catch(() => {
512
+ });
513
+ return ctx.json({ created: true, id, name }, 201);
514
+ } catch (installError) {
515
+ await fs.unlink(tmpFile).catch(() => {
516
+ });
517
+ throw installError;
518
+ }
519
+ }).openapi(getPathRoute, (ctx) => {
520
+ const agent = getAgent();
521
+ const agentPath = agent.getAgentFilePath();
522
+ const relativePath = path.basename(agentPath);
523
+ const ext = path.extname(agentPath);
524
+ const name = path.basename(agentPath, ext);
525
+ return ctx.json({
526
+ path: agentPath,
527
+ relativePath,
528
+ name,
529
+ isDefault: name === "default-agent"
530
+ });
531
+ }).openapi(getConfigRoute, async (ctx) => {
532
+ const agent = getAgent();
533
+ const agentPath = agent.getAgentFilePath();
534
+ const yamlContent = await fs.readFile(agentPath, "utf-8");
535
+ const stats = await fs.stat(agentPath);
536
+ return ctx.json({
537
+ yaml: yamlContent,
538
+ path: agentPath,
539
+ relativePath: path.basename(agentPath),
540
+ lastModified: stats.mtime,
541
+ warnings: [
542
+ "Environment variables ($VAR) will be resolved at runtime",
543
+ "API keys should use environment variables"
544
+ ]
545
+ });
546
+ }).openapi(validateConfigRoute, async (ctx) => {
547
+ const { yaml } = ctx.req.valid("json");
548
+ let parsed;
549
+ try {
550
+ parsed = yamlParse(yaml);
551
+ } catch (parseError) {
552
+ return ctx.json({
553
+ valid: false,
554
+ errors: [
555
+ {
556
+ line: parseError.linePos?.[0]?.line || 1,
557
+ column: parseError.linePos?.[0]?.col || 1,
558
+ message: parseError.message,
559
+ code: "YAML_PARSE_ERROR"
560
+ }
561
+ ],
562
+ warnings: []
563
+ });
564
+ }
565
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
566
+ return ctx.json({
567
+ valid: false,
568
+ errors: [
569
+ {
570
+ line: 1,
571
+ column: 1,
572
+ message: "Configuration must be a valid YAML object",
573
+ code: "INVALID_CONFIG_TYPE"
574
+ }
575
+ ],
576
+ warnings: []
577
+ });
578
+ }
579
+ const enriched = enrichAgentConfig(parsed, void 0);
580
+ const result = AgentConfigSchema.safeParse(enriched);
581
+ if (!result.success) {
582
+ const issues = zodToIssues(result.error);
583
+ const errors = issues.map((issue) => ({
584
+ path: issue.path?.join(".") ?? "root",
585
+ message: issue.message,
586
+ code: "SCHEMA_VALIDATION_ERROR"
587
+ }));
588
+ return ctx.json({
589
+ valid: false,
590
+ errors,
591
+ warnings: []
592
+ });
593
+ }
594
+ const warnings = [];
595
+ if (parsed.llm?.apiKey && !parsed.llm.apiKey.startsWith("$")) {
596
+ warnings.push({
597
+ path: "llm.apiKey",
598
+ message: "Consider using environment variable instead of plain text",
599
+ code: "SECURITY_WARNING"
600
+ });
601
+ }
602
+ return ctx.json({
603
+ valid: true,
604
+ errors: [],
605
+ warnings
606
+ });
607
+ }).openapi(saveConfigRoute, async (ctx) => {
608
+ const agent = getAgent();
609
+ const { yaml } = ctx.req.valid("json");
610
+ let parsed;
611
+ try {
612
+ parsed = yamlParse(yaml);
613
+ } catch (parseError) {
614
+ throw new DextoValidationError([
615
+ {
616
+ code: AgentErrorCode.INVALID_CONFIG,
617
+ message: `Invalid YAML syntax: ${parseError.message}`,
618
+ scope: ErrorScope.AGENT,
619
+ type: ErrorType.USER,
620
+ severity: "error"
621
+ }
622
+ ]);
623
+ }
624
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
625
+ throw new DextoValidationError([
626
+ {
627
+ code: AgentErrorCode.INVALID_CONFIG,
628
+ message: "Configuration must be a valid YAML object",
629
+ scope: ErrorScope.AGENT,
630
+ type: ErrorType.USER,
631
+ severity: "error"
632
+ }
633
+ ]);
634
+ }
635
+ const agentPath = agent.getAgentFilePath();
636
+ const enriched = enrichAgentConfig(parsed, agentPath);
637
+ const validationResult = AgentConfigSchema.safeParse(enriched);
638
+ if (!validationResult.success) {
639
+ throw new DextoValidationError(
640
+ validationResult.error.errors.map((err) => ({
641
+ code: AgentErrorCode.INVALID_CONFIG,
642
+ message: `${err.path.join(".")}: ${err.message}`,
643
+ scope: ErrorScope.AGENT,
644
+ type: ErrorType.USER,
645
+ severity: "error"
646
+ }))
647
+ );
648
+ }
649
+ const backupPath = `${agentPath}.backup`;
650
+ await fs.copyFile(agentPath, backupPath);
651
+ try {
652
+ await fs.writeFile(agentPath, yaml, "utf-8");
653
+ const newConfig = await reloadAgentConfigFromFile(agentPath);
654
+ const enrichedConfig = enrichAgentConfig(newConfig, agentPath);
655
+ const reloadResult = await agent.reload(enrichedConfig);
656
+ if (reloadResult.restarted) {
657
+ logger.info(
658
+ `Agent restarted to apply changes: ${reloadResult.changesApplied.join(", ")}`
659
+ );
660
+ } else if (reloadResult.changesApplied.length === 0) {
661
+ logger.info("Configuration saved (no changes detected)");
662
+ }
663
+ await fs.unlink(backupPath).catch(() => {
664
+ });
665
+ logger.info(`Agent configuration saved and applied: ${agentPath}`);
666
+ return ctx.json({
667
+ ok: true,
668
+ path: agentPath,
669
+ reloaded: true,
670
+ restarted: reloadResult.restarted,
671
+ changesApplied: reloadResult.changesApplied,
672
+ message: reloadResult.restarted ? "Configuration saved and applied successfully (agent restarted)" : "Configuration saved successfully (no changes detected)"
673
+ });
674
+ } catch (error) {
675
+ await fs.copyFile(backupPath, agentPath).catch(() => {
676
+ });
677
+ throw error;
678
+ }
679
+ }).openapi(exportConfigRoute, async (ctx) => {
680
+ const agent = getAgent();
681
+ const { sessionId } = ctx.req.valid("query");
682
+ const config = agent.getEffectiveConfig(sessionId);
683
+ const maskedConfig = {
684
+ ...config,
685
+ llm: {
686
+ ...config.llm,
687
+ apiKey: config.llm.apiKey ? "[REDACTED]" : void 0
688
+ },
689
+ mcpServers: config.mcpServers ? Object.fromEntries(
690
+ Object.entries(config.mcpServers).map(([name, serverConfig]) => [
691
+ name,
692
+ serverConfig.type === "stdio" && serverConfig.env ? {
693
+ ...serverConfig,
694
+ env: Object.fromEntries(
695
+ Object.keys(serverConfig.env).map((key) => [
696
+ key,
697
+ "[REDACTED]"
698
+ ])
699
+ )
700
+ } : serverConfig
701
+ ])
702
+ ) : void 0
703
+ };
704
+ const yamlStr = yamlStringify(maskedConfig);
705
+ ctx.header("Content-Type", "application/x-yaml");
706
+ return ctx.body(yamlStr);
707
+ });
708
+ }
709
+ export {
710
+ createAgentsRouter
711
+ };