dexto 1.1.10 → 1.2.0

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 (181) hide show
  1. package/README.md +222 -84
  2. package/dist/agents/agent-registry.json +39 -1
  3. package/dist/agents/agent-template.yml +1 -1
  4. package/dist/agents/coding-agent/README.md +188 -0
  5. package/dist/agents/coding-agent/coding-agent.yml +203 -0
  6. package/dist/agents/database-agent/database-agent.yml +44 -2
  7. package/dist/agents/default-agent.yml +137 -13
  8. package/dist/agents/github-agent/github-agent.yml +42 -0
  9. package/dist/agents/image-editor-agent/image-editor-agent.yml +9 -2
  10. package/dist/agents/music-agent/README.md +1 -1
  11. package/dist/agents/music-agent/music-agent.yml +36 -2
  12. package/dist/agents/nano-banana-agent/nano-banana-agent.yml +35 -1
  13. package/dist/agents/podcast-agent/README.md +1 -1
  14. package/dist/agents/podcast-agent/podcast-agent.yml +37 -3
  15. package/dist/agents/product-name-researcher/product-name-researcher.yml +37 -1
  16. package/dist/agents/sora-video-agent/README.md +122 -0
  17. package/dist/agents/sora-video-agent/sora-video-agent.yml +98 -0
  18. package/dist/agents/talk2pdf-agent/talk2pdf-agent.yml +17 -2
  19. package/dist/agents/triage-demo/README.md +6 -6
  20. package/dist/agents/triage-demo/billing-agent.yml +1 -1
  21. package/dist/agents/triage-demo/escalation-agent.yml +1 -1
  22. package/dist/agents/triage-demo/product-info-agent.yml +1 -1
  23. package/dist/agents/triage-demo/technical-support-agent.yml +1 -1
  24. package/dist/agents/triage-demo/triage-agent.yml +16 -1
  25. package/dist/analytics/wrapper.d.ts.map +1 -1
  26. package/dist/analytics/wrapper.js +5 -3
  27. package/dist/api/a2a.d.ts +2 -2
  28. package/dist/api/a2a.d.ts.map +1 -1
  29. package/dist/api/a2a.js +3 -2
  30. package/dist/api/mcp/mcp_handler.d.ts +3 -3
  31. package/dist/api/mcp/mcp_handler.d.ts.map +1 -1
  32. package/dist/api/mcp/mcp_handler.js +7 -4
  33. package/dist/api/mcp/tool-aggregation-handler.d.ts.map +1 -1
  34. package/dist/api/mcp/tool-aggregation-handler.js +34 -42
  35. package/dist/api/memory/memory-handler.d.ts +18 -0
  36. package/dist/api/memory/memory-handler.d.ts.map +1 -0
  37. package/dist/api/memory/memory-handler.js +137 -0
  38. package/dist/api/middleware/errorHandler.d.ts.map +1 -1
  39. package/dist/api/middleware/errorHandler.js +2 -0
  40. package/dist/api/server.d.ts +2 -2
  41. package/dist/api/server.d.ts.map +1 -1
  42. package/dist/api/server.js +1129 -257
  43. package/dist/api/webhook-subscriber.d.ts.map +1 -1
  44. package/dist/api/webhook-subscriber.js +2 -1
  45. package/dist/api/websocket-subscriber.d.ts.map +1 -1
  46. package/dist/api/websocket-subscriber.js +67 -10
  47. package/dist/cli/cli-subscriber.d.ts +2 -1
  48. package/dist/cli/cli-subscriber.d.ts.map +1 -1
  49. package/dist/cli/cli-subscriber.js +11 -3
  50. package/dist/cli/cli.d.ts.map +1 -1
  51. package/dist/cli/cli.js +1 -0
  52. package/dist/cli/commands/install.d.ts +3 -3
  53. package/dist/cli/commands/install.d.ts.map +1 -1
  54. package/dist/cli/commands/install.js +223 -41
  55. package/dist/cli/commands/interactive-commands/model/model-commands.js +2 -2
  56. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts +8 -1
  57. package/dist/cli/commands/interactive-commands/prompt-commands.d.ts.map +1 -1
  58. package/dist/cli/commands/interactive-commands/prompt-commands.js +252 -4
  59. package/dist/cli/commands/list-agents.d.ts.map +1 -1
  60. package/dist/cli/commands/list-agents.js +22 -3
  61. package/dist/cli/commands/setup.d.ts +4 -4
  62. package/dist/cli/commands/uninstall.d.ts +1 -1
  63. package/dist/cli/tool-confirmation/cli-confirmation-handler.d.ts +36 -7
  64. package/dist/cli/tool-confirmation/cli-confirmation-handler.d.ts.map +1 -1
  65. package/dist/cli/tool-confirmation/cli-confirmation-handler.js +314 -34
  66. package/dist/cli/utils/options.js +2 -2
  67. package/dist/index.js +117 -64
  68. package/dist/webui/.next/standalone/.next/static/chunks/419-6d449dcb2b056299.js +1 -0
  69. package/dist/webui/.next/standalone/.next/static/chunks/614-3519f8a6051e0088.js +1 -0
  70. package/dist/webui/.next/standalone/.next/static/chunks/656-5a9f6405badf66a8.js +25 -0
  71. package/dist/webui/.next/standalone/.next/static/chunks/765-755286dc586b1a51.js +1 -0
  72. package/dist/webui/.next/standalone/.next/static/chunks/804-f40df92a3adffcc0.js +1 -0
  73. package/dist/webui/.next/standalone/.next/static/chunks/854-232126f3c77e6c0b.js +1 -0
  74. package/dist/webui/.next/standalone/.next/static/chunks/app/chat/[sessionId]/page-a695b09e6bac5274.js +1 -0
  75. package/dist/webui/.next/standalone/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  76. package/dist/webui/.next/standalone/.next/static/chunks/app/page-d1f127a0cac96246.js +1 -0
  77. package/dist/webui/.next/standalone/.next/static/chunks/app/playground/page-6f8d2abe76e51dfc.js +1 -0
  78. package/dist/webui/.next/standalone/.next/static/chunks/main-7decd42f62688419.js +1 -0
  79. package/dist/webui/.next/standalone/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  80. package/dist/webui/.next/standalone/.next/static/css/c3c26ec984df1deb.css +1 -0
  81. package/dist/webui/.next/standalone/.next/static/css/de70bee13400563f.css +1 -0
  82. package/dist/webui/.next/standalone/.next/static/uqfH8SY_uhwdc0ZkpMwCO/_buildManifest.js +1 -0
  83. package/dist/webui/.next/standalone/package.json +7 -2
  84. package/dist/webui/.next/standalone/packages/webui/.next/BUILD_ID +1 -1
  85. package/dist/webui/.next/standalone/packages/webui/.next/app-build-manifest.json +30 -15
  86. package/dist/webui/.next/standalone/packages/webui/.next/app-path-routes-manifest.json +1 -0
  87. package/dist/webui/.next/standalone/packages/webui/.next/build-manifest.json +7 -7
  88. package/dist/webui/.next/standalone/packages/webui/.next/prerender-manifest.json +3 -3
  89. package/dist/webui/.next/standalone/packages/webui/.next/required-server-files.json +1 -11
  90. package/dist/webui/.next/standalone/packages/webui/.next/routes-manifest.json +11 -8
  91. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page.js +1 -1
  92. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page.js.nft.json +1 -1
  93. package/dist/webui/.next/standalone/packages/webui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  94. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page.js +2 -0
  95. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page.js.nft.json +1 -0
  96. package/dist/webui/.next/standalone/packages/webui/.next/server/app/chat/[sessionId]/page_client-reference-manifest.js +1 -0
  97. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page.js +2 -11
  98. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page.js.nft.json +1 -1
  99. package/dist/webui/.next/standalone/packages/webui/.next/server/app/page_client-reference-manifest.js +1 -1
  100. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page.js +4 -8
  101. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page.js.nft.json +1 -1
  102. package/dist/webui/.next/standalone/packages/webui/.next/server/app/playground/page_client-reference-manifest.js +1 -1
  103. package/dist/webui/.next/standalone/packages/webui/.next/server/app-paths-manifest.json +1 -0
  104. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/1.js +12 -0
  105. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/102.js +25 -0
  106. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/383.js +1 -0
  107. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/{619.js → 426.js} +2 -2
  108. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/43.js +1 -1
  109. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/985.js +5 -0
  110. package/dist/webui/.next/standalone/packages/webui/.next/server/middleware-build-manifest.js +1 -1
  111. package/dist/webui/.next/standalone/packages/webui/.next/server/pages/500.html +1 -1
  112. package/dist/webui/.next/standalone/packages/webui/.next/server/server-reference-manifest.json +1 -1
  113. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/419-6d449dcb2b056299.js +1 -0
  114. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/614-3519f8a6051e0088.js +1 -0
  115. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/656-5a9f6405badf66a8.js +25 -0
  116. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/765-755286dc586b1a51.js +1 -0
  117. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/804-f40df92a3adffcc0.js +1 -0
  118. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/854-232126f3c77e6c0b.js +1 -0
  119. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/chat/[sessionId]/page-a695b09e6bac5274.js +1 -0
  120. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  121. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/page-d1f127a0cac96246.js +1 -0
  122. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/playground/page-6f8d2abe76e51dfc.js +1 -0
  123. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/main-7decd42f62688419.js +1 -0
  124. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  125. package/dist/webui/.next/standalone/packages/webui/.next/static/css/c3c26ec984df1deb.css +1 -0
  126. package/dist/webui/.next/standalone/packages/webui/.next/static/css/de70bee13400563f.css +1 -0
  127. package/dist/webui/.next/standalone/packages/webui/.next/static/uqfH8SY_uhwdc0ZkpMwCO/_buildManifest.js +1 -0
  128. package/dist/webui/.next/standalone/packages/webui/package.json +11 -4
  129. package/dist/webui/.next/standalone/packages/webui/server.js +1 -1
  130. package/dist/webui/.next/static/chunks/419-6d449dcb2b056299.js +1 -0
  131. package/dist/webui/.next/static/chunks/614-3519f8a6051e0088.js +1 -0
  132. package/dist/webui/.next/static/chunks/656-5a9f6405badf66a8.js +25 -0
  133. package/dist/webui/.next/static/chunks/765-755286dc586b1a51.js +1 -0
  134. package/dist/webui/.next/static/chunks/804-f40df92a3adffcc0.js +1 -0
  135. package/dist/webui/.next/static/chunks/854-232126f3c77e6c0b.js +1 -0
  136. package/dist/webui/.next/static/chunks/app/chat/[sessionId]/page-a695b09e6bac5274.js +1 -0
  137. package/dist/webui/.next/static/chunks/app/layout-f4a6ee5a028899d1.js +1 -0
  138. package/dist/webui/.next/static/chunks/app/page-d1f127a0cac96246.js +1 -0
  139. package/dist/webui/.next/static/chunks/app/playground/page-6f8d2abe76e51dfc.js +1 -0
  140. package/dist/webui/.next/static/chunks/main-7decd42f62688419.js +1 -0
  141. package/dist/webui/.next/static/chunks/{webpack-7c234e7e7e272295.js → webpack-7229fd0786f0483c.js} +1 -1
  142. package/dist/webui/.next/static/css/c3c26ec984df1deb.css +1 -0
  143. package/dist/webui/.next/static/css/de70bee13400563f.css +1 -0
  144. package/dist/webui/.next/static/uqfH8SY_uhwdc0ZkpMwCO/_buildManifest.js +1 -0
  145. package/dist/webui/package.json +11 -4
  146. package/package.json +5 -4
  147. package/dist/webui/.next/standalone/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  148. package/dist/webui/.next/standalone/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  149. package/dist/webui/.next/standalone/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  150. package/dist/webui/.next/standalone/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  151. package/dist/webui/.next/standalone/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  152. package/dist/webui/.next/standalone/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  153. package/dist/webui/.next/standalone/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  154. package/dist/webui/.next/standalone/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  155. package/dist/webui/.next/standalone/.next/static/chunks/main-b65ece3506a2355c.js +0 -1
  156. package/dist/webui/.next/standalone/.next/static/css/045cc65741e38fbd.css +0 -3
  157. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/549.js +0 -1
  158. package/dist/webui/.next/standalone/packages/webui/.next/server/chunks/950.js +0 -5
  159. package/dist/webui/.next/standalone/packages/webui/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  160. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  161. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  162. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  163. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  164. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  165. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  166. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  167. package/dist/webui/.next/standalone/packages/webui/.next/static/chunks/main-b65ece3506a2355c.js +0 -1
  168. package/dist/webui/.next/standalone/packages/webui/.next/static/css/045cc65741e38fbd.css +0 -3
  169. package/dist/webui/.next/static/PvkEd_BO6ZXxX99T_gUvs/_buildManifest.js +0 -1
  170. package/dist/webui/.next/static/chunks/179-78abc2eacbc41da9.js +0 -1
  171. package/dist/webui/.next/static/chunks/442-b1916bec348454b3.js +0 -1
  172. package/dist/webui/.next/static/chunks/544-c4a8f278ed1a25d7.js +0 -1
  173. package/dist/webui/.next/static/chunks/854-2a6d5a5297a15d52.js +0 -1
  174. package/dist/webui/.next/static/chunks/app/layout-dde711766eda096b.js +0 -1
  175. package/dist/webui/.next/static/chunks/app/page-5e94d5a49dc718d0.js +0 -1
  176. package/dist/webui/.next/static/chunks/app/playground/page-9ae40e0b219583e3.js +0 -1
  177. package/dist/webui/.next/static/chunks/main-b65ece3506a2355c.js +0 -1
  178. package/dist/webui/.next/static/css/045cc65741e38fbd.css +0 -3
  179. /package/dist/webui/.next/standalone/.next/static/{PvkEd_BO6ZXxX99T_gUvs → uqfH8SY_uhwdc0ZkpMwCO}/_ssgManifest.js +0 -0
  180. /package/dist/webui/.next/standalone/packages/webui/.next/static/{PvkEd_BO6ZXxX99T_gUvs → uqfH8SY_uhwdc0ZkpMwCO}/_ssgManifest.js +0 -0
  181. /package/dist/webui/.next/static/{PvkEd_BO6ZXxX99T_gUvs → uqfH8SY_uhwdc0ZkpMwCO}/_ssgManifest.js +0 -0
@@ -3,23 +3,28 @@ import http from 'http';
3
3
  import { WebSocketServer } from 'ws';
4
4
  import { WebSocketEventSubscriber } from './websocket-subscriber.js';
5
5
  import { WebhookEventSubscriber } from './webhook-subscriber.js';
6
- import { logger, redactSensitiveData } from '@dexto/core';
6
+ import { logger, redactSensitiveData, deriveDisplayName } from '@dexto/core';
7
7
  import { setupA2ARoutes } from './a2a.js';
8
+ import { setupMemoryRoutes } from './memory/memory-handler.js';
8
9
  import { createMcpTransport, initializeMcpServer, initializeMcpServerApiEndpoints, } from './mcp/mcp_handler.js';
9
- import { createAgentCard, DextoAgent } from '@dexto/core';
10
- import { stringify as yamlStringify } from 'yaml';
10
+ import { createAgentCard, Dexto, DextoAgent, loadAgentConfig } from '@dexto/core';
11
+ import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
11
12
  import os from 'os';
13
+ import { promises as fs } from 'fs';
14
+ import path from 'path';
12
15
  import { expressRedactionMiddleware } from './middleware/expressRedactionMiddleware.js';
13
16
  import { z } from 'zod';
14
17
  import { LLMUpdatesSchema } from '@dexto/core';
15
18
  import { registerGracefulShutdown } from '../utils/graceful-shutdown.js';
16
19
  import { validateInputForLLM } from '@dexto/core';
17
20
  import { LLM_REGISTRY, LLM_PROVIDERS, LLM_ROUTERS, SUPPORTED_FILE_TYPES, getSupportedRoutersForProvider, supportsBaseURL, isRouterSupportedForModel, } from '@dexto/core';
18
- import { getProviderKeyStatus, saveProviderApiKey } from '@dexto/core';
21
+ import { getProviderKeyStatus, saveProviderApiKey, getPrimaryApiKeyEnvVar } from '@dexto/core';
19
22
  import { errorHandler } from './middleware/errorHandler.js';
20
23
  import { McpServerConfigSchema } from '@dexto/core';
21
24
  import { sendWebSocketError, sendWebSocketValidationError } from './websocket-error-handler.js';
22
- import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode, AgentError, } from '@dexto/core';
25
+ import { DextoValidationError, ErrorScope, ErrorType, AgentErrorCode, AgentError, AgentConfigSchema, ApprovalResponseSchema, } from '@dexto/core';
26
+ import { ResourceError } from '@dexto/core';
27
+ import { PromptError } from '@dexto/core';
23
28
  /**
24
29
  * Helper function to send JSON response with optional pretty printing
25
30
  */
@@ -34,58 +39,6 @@ function sendJsonResponse(res, data, statusCode = 200) {
34
39
  res.json(data);
35
40
  }
36
41
  }
37
- // Note: Request body may include a sessionId alongside LLM updates.
38
- // We parse sessionId separately and validate the rest against LLMUpdatesSchema
39
- /**
40
- * API request validation schemas based on actual usage
41
- */
42
- const MessageRequestSchema = z
43
- .object({
44
- message: z.string().optional(),
45
- sessionId: z.string().optional(),
46
- stream: z.boolean().optional(),
47
- imageData: z
48
- .object({
49
- base64: z.string(),
50
- mimeType: z.string(),
51
- })
52
- .optional(),
53
- fileData: z
54
- .object({
55
- base64: z.string(),
56
- mimeType: z.string(),
57
- filename: z.string().optional(),
58
- })
59
- .optional(),
60
- })
61
- .refine((data) => {
62
- const msg = (data.message ?? '').trim();
63
- // Must have either message text, image data, or file data
64
- return msg.length > 0 || !!data.imageData || !!data.fileData;
65
- }, { message: 'Must provide either message text, image data, or file data' });
66
- // Reuse existing MCP server config schema
67
- const McpServerRequestSchema = z.object({
68
- name: z.string().min(1, 'Server name is required'),
69
- config: McpServerConfigSchema,
70
- });
71
- // Based on existing WebhookRegistrationRequest interface
72
- const WebhookRequestSchema = z.object({
73
- url: z.string().url('Invalid URL format'),
74
- secret: z.string().optional(),
75
- description: z.string().optional(),
76
- });
77
- // Schema for search query parameters
78
- const SearchQuerySchema = z.object({
79
- q: z.string().min(1, 'Search query is required'),
80
- limit: z.coerce.number().min(1).max(100).optional(),
81
- offset: z.coerce.number().min(0).optional(),
82
- sessionId: z.string().optional(),
83
- role: z.enum(['user', 'assistant', 'system', 'tool']).optional(),
84
- });
85
- // Schema for cancel request parameters
86
- const CancelRequestSchema = z.object({
87
- sessionId: z.string().min(1, 'Session ID is required'),
88
- });
89
42
  // Helper to parse and validate request body
90
43
  function parseBody(schema, body) {
91
44
  return schema.parse(body); // ZodError handled by error middleware
@@ -95,19 +48,62 @@ function parseQuery(schema, query) {
95
48
  return schema.parse(query); // ZodError handled by error middleware
96
49
  }
97
50
  // TODO: API endpoint names are work in progress and might be refactored/renamed in future versions
98
- export async function initializeApi(agent, agentCardOverride, listenPort, agentName) {
51
+ export async function initializeApi(agent, agentCardOverride, listenPort, agentId) {
99
52
  const app = express();
100
53
  // Declare before registering shutdown hook to avoid TDZ on signals
101
54
  let activeAgent = agent;
102
- let activeAgentName = agentName || 'default';
55
+ let activeAgentId = agentId || 'default-agent';
103
56
  let isSwitchingAgent = false;
104
57
  registerGracefulShutdown(() => activeAgent);
58
+ // CORS middleware to allow frontend to connect from different ports
59
+ app.use((req, res, next) => {
60
+ const origin = req.headers.origin;
61
+ // Define allowed origins based on environment
62
+ const allowedOrigins = [];
63
+ // 1. Always allow localhost/127.0.0.1 on any port (for local development)
64
+ if (origin) {
65
+ const originUrl = new URL(origin);
66
+ const hostname = originUrl.hostname;
67
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
68
+ allowedOrigins.push(origin);
69
+ }
70
+ }
71
+ // 2. Allow custom origins from environment variable (for production/network deployments)
72
+ const customOrigins = process.env.DEXTO_ALLOWED_ORIGINS;
73
+ if (customOrigins) {
74
+ allowedOrigins.push(...customOrigins.split(',').map((o) => o.trim()));
75
+ }
76
+ // 3. Set CORS headers
77
+ if (origin && allowedOrigins.includes(origin)) {
78
+ res.setHeader('Access-Control-Allow-Origin', origin);
79
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
80
+ }
81
+ else if (allowedOrigins.length === 0 && !origin) {
82
+ // If no origin header (e.g., server-to-server), allow it
83
+ res.setHeader('Access-Control-Allow-Origin', '*');
84
+ }
85
+ // If origin is not allowed, don't set CORS headers (browser will block)
86
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD');
87
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
88
+ // Handle preflight requests
89
+ if (req.method === 'OPTIONS') {
90
+ return res.status(204).end();
91
+ }
92
+ return next();
93
+ });
105
94
  // this will apply middleware to all /api/llm/* routes
106
95
  app.use('/api/llm', expressRedactionMiddleware);
107
96
  app.use('/api/config.yaml', expressRedactionMiddleware);
108
97
  const server = http.createServer(app);
109
98
  const wss = new WebSocketServer({ server });
110
- logger.info(`Initializing API server with agent: ${activeAgentName}`);
99
+ logger.info(`Initializing API server with agent: ${activeAgentId}`);
100
+ // Initialize event subscribers
101
+ const webSubscriber = new WebSocketEventSubscriber(wss);
102
+ const webhookSubscriber = new WebhookEventSubscriber();
103
+ // Register subscribers before starting agent
104
+ logger.info('Registering event subscribers with agent...');
105
+ activeAgent.registerSubscriber(webSubscriber);
106
+ activeAgent.registerSubscriber(webhookSubscriber);
111
107
  // Ensure the initial agent is started
112
108
  if (!activeAgent.isStarted() && !activeAgent.isStopped()) {
113
109
  logger.info('Starting initial agent...');
@@ -116,18 +112,11 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
116
112
  else if (activeAgent.isStopped()) {
117
113
  logger.warn('Initial agent is stopped, this may cause issues');
118
114
  }
119
- const webSubscriber = new WebSocketEventSubscriber(wss);
120
- logger.info('Setting up API event subscriptions...');
121
- webSubscriber.subscribe(activeAgent.agentEventBus);
122
- // Initialize webhook subscriber
123
- const webhookSubscriber = new WebhookEventSubscriber();
124
- logger.info('Setting up webhook event subscriptions...');
125
- webhookSubscriber.subscribe(activeAgent.agentEventBus);
126
115
  // Tool confirmation responses are handled by the main WebSocket handler below
127
116
  function ensureAgentAvailable() {
128
117
  // Gate requests during agent switching
129
118
  if (isSwitchingAgent) {
130
- throw AgentError.apiValidationError('Agent switch already in progress');
119
+ throw AgentError.switchInProgress();
131
120
  }
132
121
  // Fast path: most common case is agent is started and running
133
122
  if (activeAgent.isStarted() && !activeAgent.isStopped()) {
@@ -141,53 +130,103 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
141
130
  throw AgentError.notStarted();
142
131
  }
143
132
  }
144
- async function switchAgentByName(name) {
133
+ /**
134
+ * Common agent switching logic shared by switchAgentById and switchAgentByPath.
135
+ * Handles: registering subscribers, starting agent, stopping previous agent, updating global state.
136
+ *
137
+ * @param newAgent The new DextoAgent instance to switch to
138
+ * @param agentId The identifier for the agent (used for logging and state tracking)
139
+ * @returns Agent info for the newly activated agent
140
+ */
141
+ async function performAgentSwitch(newAgent, agentId) {
142
+ // Register event subscribers with new agent before starting
143
+ logger.info('Registering event subscribers with new agent...');
144
+ newAgent.registerSubscriber(webSubscriber);
145
+ newAgent.registerSubscriber(webhookSubscriber);
146
+ logger.info(`Starting new agent: ${agentId}`);
147
+ await newAgent.start();
148
+ // Stop previous agent last (only after new one is fully operational)
149
+ const previousAgent = activeAgent;
150
+ activeAgent = newAgent;
151
+ activeAgentId = agentId;
152
+ // Update agent card for A2A and MCP routes
153
+ agentCardData = createAgentCard({
154
+ defaultName: agentId,
155
+ defaultVersion: overrides.version ?? '1.0.0',
156
+ defaultBaseUrl: baseApiUrl,
157
+ webSubscriber,
158
+ }, overrides);
159
+ logger.info(`Successfully switched to agent: ${agentId}`);
160
+ // Now safely stop the previous agent
161
+ try {
162
+ if (previousAgent && previousAgent !== newAgent) {
163
+ logger.info('Stopping previous agent...');
164
+ await previousAgent.stop();
165
+ }
166
+ }
167
+ catch (err) {
168
+ logger.warn(`Stopping previous agent failed: ${err}`);
169
+ // Don't throw here as the switch was successful
170
+ }
171
+ return await resolveAgentInfo(agentId);
172
+ }
173
+ async function switchAgentById(agentId) {
145
174
  if (isSwitchingAgent) {
146
- throw AgentError.apiValidationError('Agent switch already in progress');
175
+ throw AgentError.switchInProgress();
147
176
  }
148
177
  isSwitchingAgent = true;
149
178
  let newAgent;
150
179
  try {
151
- // Use domain layer method to create new agent
152
- newAgent = await DextoAgent.createAgent(name);
153
- logger.info(`Starting new agent: ${name}`);
154
- await newAgent.start();
155
- // Rewire event/webhook subscribers to new agent bus
156
- logger.info('Rewiring event subscribers...');
157
- try {
158
- webSubscriber.unsubscribe();
159
- }
160
- catch (_err) {
161
- logger.debug(`Failed to unsubscribe webSubscriber: ${_err instanceof Error ? _err.message : String(_err)}`);
162
- }
163
- webSubscriber.subscribe(newAgent.agentEventBus);
164
- try {
165
- webhookSubscriber.unsubscribe();
166
- }
167
- catch (_err) {
168
- logger.debug(`Failed to unsubscribe webhookSubscriber: ${_err instanceof Error ? _err.message : String(_err)}`);
169
- }
170
- webhookSubscriber.subscribe(newAgent.agentEventBus);
171
- // Stop previous agent last (only after new one is fully operational)
172
- const previousAgent = activeAgent;
173
- activeAgent = newAgent;
174
- activeAgentName = name;
175
- logger.info(`Successfully switched to agent: ${name}`);
176
- // Now safely stop the previous agent
177
- try {
178
- if (previousAgent && previousAgent !== newAgent) {
179
- logger.info('Stopping previous agent...');
180
- await previousAgent.stop();
180
+ // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
181
+ // This allows new agent to have different telemetry config (endpoint, protocol, etc.)
182
+ logger.info('Shutting down telemetry for agent switch...');
183
+ const { Telemetry } = await import('@dexto/core');
184
+ await Telemetry.shutdownGlobal();
185
+ // 2. Create new agent from registry (will initialize fresh telemetry in createAgentServices)
186
+ newAgent = await Dexto.createAgent(agentId);
187
+ // 3. Use common switch logic (register subscribers, start agent, stop previous)
188
+ return await performAgentSwitch(newAgent, agentId);
189
+ }
190
+ catch (error) {
191
+ logger.error(`Failed to switch to agent '${agentId}': ${error instanceof Error ? error.message : String(error)}`, { error });
192
+ // Clean up the failed new agent if it was created
193
+ if (newAgent) {
194
+ try {
195
+ await newAgent.stop();
196
+ }
197
+ catch (cleanupErr) {
198
+ logger.warn(`Failed to cleanup new agent: ${cleanupErr}`);
181
199
  }
182
200
  }
183
- catch (err) {
184
- logger.warn(`Stopping previous agent failed: ${err}`);
185
- // Don't throw here as the switch was successful
186
- }
187
- return { name };
201
+ throw error;
202
+ }
203
+ finally {
204
+ isSwitchingAgent = false;
205
+ }
206
+ }
207
+ async function switchAgentByPath(filePath) {
208
+ if (isSwitchingAgent) {
209
+ throw AgentError.switchInProgress();
210
+ }
211
+ isSwitchingAgent = true;
212
+ let newAgent;
213
+ try {
214
+ // 1. SHUTDOWN OLD TELEMETRY FIRST (before creating new agent)
215
+ // This allows new agent to have different telemetry config (endpoint, protocol, etc.)
216
+ logger.info('Shutting down telemetry for agent switch...');
217
+ const { Telemetry } = await import('@dexto/core');
218
+ await Telemetry.shutdownGlobal();
219
+ // 2. Load agent configuration from file path
220
+ const config = await loadAgentConfig(filePath);
221
+ // 3. Create new agent instance directly (will initialize fresh telemetry in createAgentServices)
222
+ newAgent = new DextoAgent(config, filePath);
223
+ // 4. Derive agent ID from config or filename
224
+ const agentId = config.agentCard?.name || path.basename(filePath, path.extname(filePath));
225
+ // 5. Use common switch logic (register subscribers, start agent, stop previous)
226
+ return await performAgentSwitch(newAgent, agentId);
188
227
  }
189
228
  catch (error) {
190
- logger.error(`Failed to switch to agent '${name}': ${error instanceof Error ? error.message : String(error)}`, { error });
229
+ logger.error(`Failed to switch to agent from path '${filePath}': ${error instanceof Error ? error.message : String(error)}`, { error });
191
230
  // Clean up the failed new agent if it was created
192
231
  if (newAgent) {
193
232
  try {
@@ -204,10 +243,244 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
204
243
  }
205
244
  }
206
245
  // HTTP endpoints
246
+ // ---- Helpers (local) ----
247
+ /**
248
+ * Helper to decode URI components with consistent error handling.
249
+ *
250
+ * Wraps native decodeURIComponent() to provide domain-specific error handling.
251
+ * While normally 1-line wrappers are discouraged, this is justified because:
252
+ * 1. Native TS function with no control over error type
253
+ * 2. Ensures consistent ResourceError across all URI decoding
254
+ * 3. Reused in 5+ Zod transform schemas
255
+ */
256
+ function decodeUriComponent(encoded) {
257
+ try {
258
+ return decodeURIComponent(encoded);
259
+ }
260
+ catch (_error) {
261
+ throw ResourceError.invalidUriFormat(encoded, 'valid URI-encoded resource identifier');
262
+ }
263
+ }
264
+ /**
265
+ * Helper function to redact sensitive environment variables
266
+ */
267
+ function redactEnvValue(value) {
268
+ if (value && typeof value === 'string' && value.length > 0) {
269
+ return '[REDACTED]';
270
+ }
271
+ return String(value ?? '');
272
+ }
273
+ /**
274
+ * Helper function to redact environment variables in a server config
275
+ */
276
+ function redactServerEnvVars(serverConfig) {
277
+ if (serverConfig.type !== 'stdio' || !serverConfig.env) {
278
+ return serverConfig;
279
+ }
280
+ const redactedEnv = {};
281
+ for (const [key, value] of Object.entries(serverConfig.env)) {
282
+ redactedEnv[key] = redactEnvValue(value);
283
+ }
284
+ return {
285
+ ...serverConfig,
286
+ env: redactedEnv,
287
+ };
288
+ }
289
+ /**
290
+ * Helper function to redact all MCP servers configuration
291
+ */
292
+ function redactMcpServersConfig(mcpServers) {
293
+ if (!mcpServers) {
294
+ return {};
295
+ }
296
+ const redactedServers = {};
297
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
298
+ redactedServers[name] = redactServerEnvVars(serverConfig);
299
+ }
300
+ return redactedServers;
301
+ }
207
302
  // Health check endpoint
208
- app.get('/health', (req, res) => {
303
+ app.get('/health', (_req, res) => {
209
304
  res.status(200).send('OK');
210
305
  });
306
+ // Prompts listing endpoint (for WebUI slash command autocomplete)
307
+ app.get('/api/prompts', async (_req, res, next) => {
308
+ try {
309
+ ensureAgentAvailable();
310
+ const prompts = await activeAgent.listPrompts();
311
+ const list = Object.values(prompts);
312
+ return res.status(200).json({ prompts: list });
313
+ }
314
+ catch (error) {
315
+ return next(error);
316
+ }
317
+ });
318
+ const CustomPromptRequestSchema = z
319
+ .object({
320
+ name: z.string().min(1, 'Prompt name is required'),
321
+ title: z.string().optional(),
322
+ description: z.string().optional(),
323
+ content: z.string().min(1, 'Prompt content is required'),
324
+ arguments: z
325
+ .array(z
326
+ .object({
327
+ name: z.string().min(1, 'Argument name is required'),
328
+ description: z.string().optional(),
329
+ required: z.boolean().optional(),
330
+ })
331
+ .strict())
332
+ .optional(),
333
+ resource: z
334
+ .object({
335
+ base64: z.string().min(1, 'Resource data is required'),
336
+ mimeType: z.string().min(1, 'Resource MIME type is required'),
337
+ filename: z.string().optional(),
338
+ })
339
+ .strict()
340
+ .optional(),
341
+ })
342
+ .strict();
343
+ app.post('/api/prompts/custom', express.json({ limit: '10mb' }), async (req, res, next) => {
344
+ try {
345
+ ensureAgentAvailable();
346
+ const payload = parseBody(CustomPromptRequestSchema, req.body);
347
+ const promptArguments = payload.arguments
348
+ ?.map((arg) => ({
349
+ name: arg.name,
350
+ ...(arg.description ? { description: arg.description } : {}),
351
+ ...(typeof arg.required === 'boolean' ? { required: arg.required } : {}),
352
+ }))
353
+ .filter(Boolean);
354
+ const createPayload = {
355
+ name: payload.name,
356
+ content: payload.content,
357
+ ...(payload.title ? { title: payload.title } : {}),
358
+ ...(payload.description ? { description: payload.description } : {}),
359
+ ...(promptArguments && promptArguments.length > 0
360
+ ? { arguments: promptArguments }
361
+ : {}),
362
+ ...(payload.resource
363
+ ? {
364
+ resource: {
365
+ base64: payload.resource.base64,
366
+ mimeType: payload.resource.mimeType,
367
+ ...(payload.resource.filename
368
+ ? { filename: payload.resource.filename }
369
+ : {}),
370
+ },
371
+ }
372
+ : {}),
373
+ };
374
+ const prompt = await activeAgent.createCustomPrompt(createPayload);
375
+ return res.status(201).json({ prompt });
376
+ }
377
+ catch (error) {
378
+ return next(error);
379
+ }
380
+ });
381
+ const DeleteCustomPromptParamsSchema = z.object({
382
+ name: z
383
+ .string()
384
+ .min(1, 'Prompt name is required')
385
+ .transform((encoded) => decodeUriComponent(encoded)),
386
+ });
387
+ app.delete('/api/prompts/custom/:name', async (req, res, next) => {
388
+ try {
389
+ ensureAgentAvailable();
390
+ const { name } = parseQuery(DeleteCustomPromptParamsSchema, req.params);
391
+ await activeAgent.deleteCustomPrompt(name);
392
+ return res.status(204).send();
393
+ }
394
+ catch (error) {
395
+ return next(error);
396
+ }
397
+ });
398
+ // Get a specific prompt definition
399
+ const GetPromptDefinitionParamsSchema = z.object({
400
+ name: z.string().min(1, 'Prompt name is required'),
401
+ });
402
+ app.get('/api/prompts/:name', async (req, res, next) => {
403
+ try {
404
+ ensureAgentAvailable();
405
+ const { name } = parseQuery(GetPromptDefinitionParamsSchema, req.params);
406
+ const definition = await activeAgent.getPromptDefinition(name);
407
+ if (!definition)
408
+ throw PromptError.notFound(name);
409
+ return sendJsonResponse(res, { definition }, 200);
410
+ }
411
+ catch (error) {
412
+ return next(error);
413
+ }
414
+ });
415
+ // Resolve a prompt to text content (without sending to the agent)
416
+ // Supports optional args via query string. For natural language after the
417
+ // slash command, pass as `context`.
418
+ const ResolvePromptParamsSchema = z.object({
419
+ name: z.string().min(1, 'Prompt name is required'),
420
+ });
421
+ const ResolvePromptQuerySchema = z.object({
422
+ context: z.string().optional(),
423
+ args: z.string().optional(),
424
+ });
425
+ app.get('/api/prompts/:name/resolve', async (req, res, next) => {
426
+ try {
427
+ ensureAgentAvailable();
428
+ const { name: inputName } = parseQuery(ResolvePromptParamsSchema, req.params);
429
+ const { context, args: argsString } = parseQuery(ResolvePromptQuerySchema, req.query);
430
+ // Optional structured args in `args` query param as JSON
431
+ let parsedArgs;
432
+ if (argsString) {
433
+ try {
434
+ const parsed = JSON.parse(argsString);
435
+ if (parsed && typeof parsed === 'object') {
436
+ parsedArgs = parsed;
437
+ }
438
+ }
439
+ catch {
440
+ // Ignore malformed args JSON; continue with whatever we have
441
+ }
442
+ }
443
+ // Build options object with only defined values (exactOptionalPropertyTypes compatibility)
444
+ const options = {};
445
+ if (context !== undefined)
446
+ options.context = context;
447
+ if (parsedArgs !== undefined)
448
+ options.args = parsedArgs;
449
+ // Use DextoAgent's resolvePrompt method
450
+ const result = await activeAgent.resolvePrompt(inputName, options);
451
+ return sendJsonResponse(res, { text: result.text, resources: result.resources }, 200);
452
+ }
453
+ catch (error) {
454
+ return next(error);
455
+ }
456
+ });
457
+ // Note: We intentionally omit an "execute" endpoint; clients resolve prompts
458
+ // and then call the regular message endpoint, keeping server surface minimal.
459
+ // Message request schema (shared by /api/message and /api/message-sync)
460
+ const MessageRequestSchema = z
461
+ .object({
462
+ message: z.string().optional(),
463
+ sessionId: z.string().optional(),
464
+ stream: z.boolean().optional(),
465
+ imageData: z
466
+ .object({
467
+ base64: z.string(),
468
+ mimeType: z.string(),
469
+ })
470
+ .optional(),
471
+ fileData: z
472
+ .object({
473
+ base64: z.string(),
474
+ mimeType: z.string(),
475
+ filename: z.string().optional(),
476
+ })
477
+ .optional(),
478
+ })
479
+ .refine((data) => {
480
+ const msg = (data.message ?? '').trim();
481
+ // Must have either message text, image data, or file data
482
+ return msg.length > 0 || !!data.imageData || !!data.fileData;
483
+ }, { message: 'Must provide either message text, image data, or file data' });
211
484
  // JSON body size limit for message endpoints supporting base64 image/file payloads
212
485
  // Both /api/message and /api/message-sync accept base64 attachments; increased limit to avoid 413s.
213
486
  app.post('/api/message', express.json({ limit: process.env.MESSAGE_JSON_LIMIT || '10mb' }), async (req, res, next) => {
@@ -240,8 +513,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
240
513
  }
241
514
  });
242
515
  // Cancel an in-flight run for a session
516
+ const CancelRequestSchema = z.object({
517
+ sessionId: z.string().min(1, 'Session ID is required'),
518
+ });
243
519
  app.post('/api/sessions/:sessionId/cancel', async (req, res, next) => {
244
520
  try {
521
+ ensureAgentAvailable();
245
522
  const { sessionId } = parseQuery(CancelRequestSchema, req.params);
246
523
  const cancelled = await activeAgent.cancel(sessionId);
247
524
  if (!cancelled) {
@@ -286,11 +563,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
286
563
  return next(error);
287
564
  }
288
565
  });
566
+ const ResetRequestSchema = z.object({
567
+ sessionId: z.string().optional(),
568
+ });
289
569
  app.post('/api/reset', express.json(), async (req, res, next) => {
290
570
  logger.info('Received request via POST /api/reset');
291
571
  try {
292
572
  ensureAgentAvailable();
293
- const { sessionId } = parseBody(z.object({ sessionId: z.string().optional() }), req.body);
573
+ const { sessionId } = parseBody(ResetRequestSchema, req.body);
294
574
  await activeAgent.resetConversation(sessionId);
295
575
  return res.status(200).send({ status: 'reset initiated', sessionId });
296
576
  }
@@ -299,25 +579,40 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
299
579
  }
300
580
  });
301
581
  // Dynamic MCP server connection endpoint (legacy)
302
- app.post('/api/connect-server', express.json(), async (req, res, next) => {
303
- try {
304
- ensureAgentAvailable();
305
- const { name, config } = parseBody(McpServerRequestSchema, req.body);
306
- await activeAgent.connectMcpServer(name, config);
307
- logger.info(`Successfully connected to new server '${name}' via API request.`);
308
- return res.status(200).send({ status: 'connected', name });
309
- }
310
- catch (error) {
311
- return next(error);
312
- }
582
+ const McpServerRequestSchema = z.object({
583
+ name: z.string().min(1, 'Server name is required'),
584
+ config: McpServerConfigSchema,
585
+ persistToAgent: z.boolean().optional(),
313
586
  });
314
587
  // Add a new MCP server
315
588
  app.post('/api/mcp/servers', express.json(), async (req, res, next) => {
316
589
  try {
317
590
  ensureAgentAvailable();
318
- const { name, config } = parseBody(McpServerRequestSchema, req.body);
591
+ const { name, config, persistToAgent } = parseBody(McpServerRequestSchema, req.body);
592
+ // Connect the server
319
593
  await activeAgent.connectMcpServer(name, config);
320
- return res.status(201).json({ status: 'connected', name });
594
+ logger.info(`Successfully connected to new server '${name}' via API request.`);
595
+ // If persistToAgent is true, save to agent config file
596
+ if (persistToAgent === true) {
597
+ try {
598
+ // Get the current effective config to read existing mcpServers
599
+ const currentConfig = activeAgent.getEffectiveConfig();
600
+ // Create update with new server added to mcpServers
601
+ const updates = {
602
+ mcpServers: {
603
+ ...(currentConfig.mcpServers || {}),
604
+ [name]: config,
605
+ },
606
+ };
607
+ await activeAgent.updateAndSaveConfig(updates);
608
+ logger.info(`Saved server '${name}' to agent configuration file`);
609
+ }
610
+ catch (saveError) {
611
+ logger.warn(`Failed to save server '${name}' to agent config:`, saveError);
612
+ // Don't fail the request if saving fails - server is still connected
613
+ }
614
+ }
615
+ return res.status(200).send({ status: 'connected', name });
321
616
  }
322
617
  catch (error) {
323
618
  return next(error);
@@ -343,10 +638,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
343
638
  }
344
639
  });
345
640
  // Add MCP server tools listing endpoint
641
+ const ListServerToolsParamsSchema = z.object({
642
+ serverId: z.string().min(1, 'Server ID is required'),
643
+ });
346
644
  app.get('/api/mcp/servers/:serverId/tools', async (req, res, next) => {
347
645
  try {
348
646
  ensureAgentAvailable();
349
- const serverId = req.params.serverId;
647
+ const { serverId } = parseQuery(ListServerToolsParamsSchema, req.params);
350
648
  const client = activeAgent.getMcpClients().get(serverId);
351
649
  if (!client) {
352
650
  return res.status(404).json({ error: `Server '${serverId}' not found` });
@@ -365,10 +663,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
365
663
  }
366
664
  });
367
665
  // Endpoint to remove/disconnect an MCP server
666
+ const DeleteMcpServerParamsSchema = z.object({
667
+ serverId: z.string().min(1, 'Server ID is required'),
668
+ });
368
669
  app.delete('/api/mcp/servers/:serverId', async (req, res, next) => {
369
- const { serverId } = req.params;
370
- logger.info(`Received request to DELETE /api/mcp/servers/${serverId}`);
371
670
  try {
671
+ ensureAgentAvailable();
672
+ const { serverId } = parseQuery(DeleteMcpServerParamsSchema, req.params);
673
+ logger.info(`Received request to DELETE /api/mcp/servers/${serverId}`);
372
674
  // Check if server exists before attempting to disconnect
373
675
  const clientExists = activeAgent.getMcpClients().has(serverId) ||
374
676
  activeAgent.getMcpFailedConnections()[serverId];
@@ -383,19 +685,45 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
383
685
  return next(error);
384
686
  }
385
687
  });
688
+ // Endpoint to restart an MCP server
689
+ const RestartMcpServerParamsSchema = z.object({
690
+ serverId: z.string().min(1, 'Server ID is required'),
691
+ });
692
+ app.post('/api/mcp/servers/:serverId/restart', async (req, res, next) => {
693
+ try {
694
+ ensureAgentAvailable();
695
+ const { serverId } = parseQuery(RestartMcpServerParamsSchema, req.params);
696
+ logger.info(`Received request to POST /api/mcp/servers/${serverId}/restart`);
697
+ // Check if server exists before attempting to restart
698
+ const clientExists = activeAgent.getMcpClients().has(serverId);
699
+ if (!clientExists) {
700
+ logger.warn(`Attempted to restart non-existent server: ${serverId}`);
701
+ return res.status(404).json({ error: `Server '${serverId}' not found.` });
702
+ }
703
+ await activeAgent.restartMcpServer(serverId);
704
+ return res.status(200).json({ status: 'restarted', id: serverId });
705
+ }
706
+ catch (error) {
707
+ return next(error);
708
+ }
709
+ });
386
710
  // Execute an MCP tool via REST wrapper
711
+ const ExecuteMcpToolParamsSchema = z.object({
712
+ serverId: z.string().min(1, 'Server ID is required'),
713
+ toolName: z.string().min(1, 'Tool name is required'),
714
+ });
387
715
  app.post('/api/mcp/servers/:serverId/tools/:toolName/execute', express.json(), async (req, res, next) => {
388
- const { serverId, toolName } = req.params;
389
- // Verify server exists
390
- const client = activeAgent.getMcpClients().get(serverId);
391
- if (!client) {
392
- return res
393
- .status(404)
394
- .json({ success: false, error: `Server '${serverId}' not found` });
395
- }
396
716
  try {
397
- // Execute tool through the agent's unified wrapper method
398
- const rawResult = await activeAgent.executeTool(toolName, req.body);
717
+ const { serverId, toolName } = parseQuery(ExecuteMcpToolParamsSchema, req.params);
718
+ // Verify server exists
719
+ const client = activeAgent.getMcpClients().get(serverId);
720
+ if (!client) {
721
+ return res
722
+ .status(404)
723
+ .json({ success: false, error: `Server '${serverId}' not found` });
724
+ }
725
+ // Execute tool directly on the specified server
726
+ const rawResult = await client.callTool(toolName, req.body);
399
727
  // Return standardized result shape
400
728
  return res.json({ success: true, data: rawResult });
401
729
  }
@@ -403,6 +731,87 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
403
731
  return next(error);
404
732
  }
405
733
  });
734
+ // ============= RESOURCE MANAGEMENT ENDPOINTS =============
735
+ // Get all available resources
736
+ app.get('/api/resources', async (_req, res, next) => {
737
+ try {
738
+ ensureAgentAvailable();
739
+ const resources = await activeAgent.listResources();
740
+ return res.status(200).json({ ok: true, resources: Object.values(resources) });
741
+ }
742
+ catch (error) {
743
+ return next(error);
744
+ }
745
+ });
746
+ // Read resource content
747
+ const ReadResourceContentParamsSchema = z.object({
748
+ resourceId: z
749
+ .string()
750
+ .min(1, 'Resource ID is required')
751
+ .transform((encoded) => decodeUriComponent(encoded)),
752
+ });
753
+ app.get('/api/resources/:resourceId/content', async (req, res, next) => {
754
+ try {
755
+ ensureAgentAvailable();
756
+ const { resourceId } = parseQuery(ReadResourceContentParamsSchema, req.params);
757
+ const content = await activeAgent.readResource(resourceId);
758
+ return res.status(200).json({ ok: true, content });
759
+ }
760
+ catch (error) {
761
+ return next(error);
762
+ }
763
+ });
764
+ // Check if resource exists
765
+ const CheckResourceExistsParamsSchema = z.object({
766
+ resourceId: z
767
+ .string()
768
+ .min(1, 'Resource ID is required')
769
+ .transform((encoded) => decodeUriComponent(encoded)),
770
+ });
771
+ app.head('/api/resources/:resourceId', async (req, res, next) => {
772
+ try {
773
+ const { resourceId } = parseQuery(CheckResourceExistsParamsSchema, req.params);
774
+ const exists = await activeAgent.hasResource(resourceId);
775
+ return res.status(exists ? 200 : 404).end();
776
+ }
777
+ catch (error) {
778
+ return next(error);
779
+ }
780
+ });
781
+ // List resources for a specific MCP server
782
+ const ListServerResourcesParamsSchema = z.object({
783
+ serverId: z.string().min(1, 'Server ID is required'),
784
+ });
785
+ app.get('/api/mcp/servers/:serverId/resources', async (req, res, next) => {
786
+ try {
787
+ ensureAgentAvailable();
788
+ const { serverId } = parseQuery(ListServerResourcesParamsSchema, req.params);
789
+ const resources = await activeAgent.listResourcesForServer(serverId);
790
+ return sendJsonResponse(res, { success: true, resources }, 200);
791
+ }
792
+ catch (error) {
793
+ return next(error);
794
+ }
795
+ });
796
+ // Read resource content from specific MCP server
797
+ const ReadServerResourceContentParamsSchema = z.object({
798
+ serverId: z.string().min(1, 'Server ID is required'),
799
+ resourceId: z
800
+ .string()
801
+ .min(1, 'Resource ID is required')
802
+ .transform((encoded) => decodeUriComponent(encoded)),
803
+ });
804
+ app.get('/api/mcp/servers/:serverId/resources/:resourceId/content', async (req, res, next) => {
805
+ try {
806
+ const { serverId, resourceId } = parseQuery(ReadServerResourceContentParamsSchema, req.params);
807
+ const qualifiedUri = `mcp:${serverId}:${resourceId}`;
808
+ const content = await activeAgent.readResource(qualifiedUri);
809
+ return sendJsonResponse(res, { success: true, data: { content } }, 200);
810
+ }
811
+ catch (error) {
812
+ return next(error);
813
+ }
814
+ });
406
815
  // WebSocket handling
407
816
  // handle inbound client messages over WebSocket
408
817
  wss.on('connection', (ws) => {
@@ -424,9 +833,16 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
424
833
  }
425
834
  try {
426
835
  const data = JSON.parse(messageString);
427
- if (data.type === 'toolConfirmationResponse' && data.data) {
428
- // Route confirmation back via AgentEventBus and do not broadcast an error
429
- activeAgent.agentEventBus.emit('dexto:toolConfirmationResponse', data.data);
836
+ if (data.type === 'approvalResponse' && data.data) {
837
+ // Validate the approval response payload with Zod schema
838
+ const validationResult = ApprovalResponseSchema.safeParse(data.data);
839
+ if (!validationResult.success) {
840
+ logger.warn(`Received invalid approval response payload: ${validationResult.error.message}`);
841
+ // Do not emit invalid payloads
842
+ return;
843
+ }
844
+ // Route validated approval response back via AgentEventBus
845
+ activeAgent.agentEventBus.emit('dexto:approvalResponse', validationResult.data);
430
846
  return;
431
847
  }
432
848
  else if (data.type === 'message' &&
@@ -458,6 +874,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
458
874
  // Check if agent is available before processing
459
875
  try {
460
876
  ensureAgentAvailable();
877
+ logger.debug('Agent availability check passed');
461
878
  }
462
879
  catch (error) {
463
880
  logger.error(`Agent not available for WebSocket message: ${error}`);
@@ -465,7 +882,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
465
882
  return;
466
883
  }
467
884
  // Comprehensive input validation
885
+ logger.debug('Getting effective config for validation');
468
886
  const currentConfig = activeAgent.getEffectiveConfig(sessionId);
887
+ logger.debug('Validating input for LLM');
469
888
  const validation = validateInputForLLM({
470
889
  text: data.content,
471
890
  ...(imageDataInput && { imageData: imageDataInput }),
@@ -501,7 +920,9 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
501
920
  sendWebSocketError(ws, hierarchicalError, sessionId);
502
921
  return;
503
922
  }
923
+ logger.debug('Validation passed, calling activeAgent.run()');
504
924
  await activeAgent.run(data.content, imageDataInput, fileDataInput, sessionId, stream);
925
+ logger.debug('activeAgent.run() completed');
505
926
  }
506
927
  else if (data.type === 'reset') {
507
928
  const sessionId = data.sessionId;
@@ -577,7 +998,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
577
998
  const overrides = agentCardOverride ?? {};
578
999
  const resolvedPort = typeof listenPort === 'number' ? listenPort : Number(process.env.PORT || 3000);
579
1000
  const baseApiUrl = process.env.DEXTO_BASE_URL || `http://localhost:${resolvedPort}`;
580
- const agentCardData = createAgentCard({
1001
+ let agentCardData = createAgentCard({
581
1002
  defaultName: overrides.name ?? 'dexto',
582
1003
  defaultVersion: overrides.version ?? '1.0.0',
583
1004
  defaultBaseUrl: baseApiUrl,
@@ -586,17 +1007,15 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
586
1007
  const _agentName = agentCardData.name;
587
1008
  const _agentVersion = agentCardData.version;
588
1009
  // Setup A2A routes
589
- setupA2ARoutes(app, agentCardData);
1010
+ setupA2ARoutes(app, () => agentCardData);
1011
+ // Setup Memory routes
1012
+ app.use('/api/memory', setupMemoryRoutes(() => activeAgent));
590
1013
  // --- Initialize and Setup MCP Server and Endpoints ---
591
1014
  // Get transport type from environment variable or default to http
592
1015
  try {
593
1016
  const transportType = process.env.DEXTO_MCP_TRANSPORT_TYPE || 'http';
594
1017
  const mcpTransport = await createMcpTransport(transportType);
595
- // TODO: MCP server is bound to the initial agent; breaks after agent switch
596
- // initializeMcpServer receives the original agent, so MCP endpoints keep talking to the stale instance post-switch.
597
- // Make MCP consume the current agent via a getter to stay in sync.
598
- await initializeMcpServer(agent, agentCardData, // Pass the agent card data for the MCP resource
599
- mcpTransport);
1018
+ await initializeMcpServer(() => activeAgent, () => agentCardData, mcpTransport);
600
1019
  await initializeMcpServerApiEndpoints(app, mcpTransport);
601
1020
  }
602
1021
  catch (error) {
@@ -607,14 +1026,30 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
607
1026
  });
608
1027
  }
609
1028
  // ===== Agents API =====
1029
+ // TODO: Consider moving to AgentRegistry.getAgentInfo() if this pattern is needed
1030
+ // outside of API response formatting (e.g., in CLI commands, WebUI hooks, client SDK)
1031
+ /**
1032
+ * Helper to resolve agent ID to { id, name } by looking up in registry
1033
+ * @param agentId - The agent ID to resolve
1034
+ * @returns Object with id and name (uses deriveDisplayName as fallback)
1035
+ */
1036
+ async function resolveAgentInfo(agentId) {
1037
+ const agents = await Dexto.listAgents();
1038
+ const agent = agents.installed.find((a) => a.id === agentId) ??
1039
+ agents.available.find((a) => a.id === agentId);
1040
+ return {
1041
+ id: agentId,
1042
+ name: agent?.name ?? deriveDisplayName(agentId),
1043
+ };
1044
+ }
610
1045
  app.get('/api/agents', async (_req, res, next) => {
611
1046
  try {
612
- ensureAgentAvailable();
613
- const agents = await activeAgent.listAgents();
1047
+ const agents = await Dexto.listAgents();
1048
+ const currentId = activeAgentId ?? null;
614
1049
  return sendJsonResponse(res, {
615
1050
  installed: agents.installed,
616
1051
  available: agents.available,
617
- current: { name: activeAgentName ?? 'default' },
1052
+ current: currentId ? await resolveAgentInfo(currentId) : { id: null, name: null },
618
1053
  });
619
1054
  }
620
1055
  catch (error) {
@@ -623,20 +1058,102 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
623
1058
  });
624
1059
  app.get('/api/agents/current', async (_req, res, next) => {
625
1060
  try {
626
- // TODO: Consider exposing agent.getName() method or config.name for more accurate tracking
627
- return sendJsonResponse(res, { name: activeAgentName ?? 'default' });
1061
+ const currentId = activeAgentId ?? null;
1062
+ if (!currentId) {
1063
+ return sendJsonResponse(res, { id: null, name: null });
1064
+ }
1065
+ return sendJsonResponse(res, await resolveAgentInfo(currentId));
628
1066
  }
629
1067
  catch (error) {
630
1068
  return next(error);
631
1069
  }
632
1070
  });
633
- const AgentNameSchema = z.object({ name: z.string().min(1) }).strict();
1071
+ const AgentIdentifierSchema = z
1072
+ .object({
1073
+ id: z
1074
+ .string()
1075
+ .min(1, 'Agent id is required')
1076
+ .describe('Unique agent identifier (e.g., "database-agent")'),
1077
+ path: z
1078
+ .string()
1079
+ .optional()
1080
+ .describe('Optional absolute file path for file-based agents (e.g., "/path/to/agent.yml")'),
1081
+ })
1082
+ .strict();
1083
+ const UninstallAgentSchema = z
1084
+ .object({
1085
+ id: z
1086
+ .string()
1087
+ .min(1, 'Agent id is required')
1088
+ .describe('Unique agent identifier to uninstall'),
1089
+ force: z
1090
+ .boolean()
1091
+ .default(false)
1092
+ .describe('Force uninstall even if agent is currently active'),
1093
+ })
1094
+ .strict();
1095
+ // Schema for custom agent installation (CLI/automation entrypoint)
1096
+ const CustomAgentInstallSchema = z
1097
+ .object({
1098
+ id: z.string().min(1, 'Agent id is required').describe('Unique agent identifier'),
1099
+ name: z.string().optional().describe('Display name (defaults to derived from id)'),
1100
+ sourcePath: z.string().min(1).describe('Path to agent configuration file or directory'),
1101
+ metadata: z
1102
+ .object({
1103
+ description: z
1104
+ .string()
1105
+ .min(1)
1106
+ .describe('Human-readable description of the agent'),
1107
+ author: z.string().min(1).describe('Agent author or organization name'),
1108
+ tags: z.array(z.string()).describe('Tags for categorizing the agent'),
1109
+ main: z
1110
+ .string()
1111
+ .optional()
1112
+ .describe('Main configuration file name within source directory'),
1113
+ })
1114
+ .strict(),
1115
+ injectPreferences: z
1116
+ .boolean()
1117
+ .default(true)
1118
+ .describe('Whether to inject user preferences into agent config'),
1119
+ })
1120
+ .strict()
1121
+ .transform((value) => {
1122
+ const displayName = value.name?.trim() || deriveDisplayName(value.id);
1123
+ return {
1124
+ id: value.id,
1125
+ displayName,
1126
+ sourcePath: value.sourcePath,
1127
+ metadata: value.metadata,
1128
+ injectPreferences: value.injectPreferences,
1129
+ };
1130
+ });
634
1131
  app.post('/api/agents/install', express.json(), async (req, res, next) => {
635
1132
  try {
636
- ensureAgentAvailable();
637
- const { name } = AgentNameSchema.parse(req.body);
638
- await activeAgent.installAgent(name);
639
- return sendJsonResponse(res, { installed: true, name }, 201);
1133
+ // Check if this is a custom agent installation (has sourcePath and metadata)
1134
+ if (req.body.sourcePath && req.body.metadata) {
1135
+ const { id, displayName, sourcePath, metadata, injectPreferences } = CustomAgentInstallSchema.parse(req.body);
1136
+ // Clean metadata to match exact optional property types
1137
+ await Dexto.installCustomAgent(id, sourcePath, {
1138
+ name: displayName,
1139
+ description: metadata.description,
1140
+ author: metadata.author,
1141
+ tags: metadata.tags,
1142
+ ...(metadata.main ? { main: metadata.main } : {}),
1143
+ }, injectPreferences);
1144
+ return sendJsonResponse(res, { installed: true, id, name: displayName, type: 'custom' }, 201);
1145
+ }
1146
+ else {
1147
+ // Registry agent installation
1148
+ const { id } = parseBody(AgentIdentifierSchema, req.body);
1149
+ await Dexto.installAgent(id);
1150
+ const agentInfo = await resolveAgentInfo(id);
1151
+ return sendJsonResponse(res, {
1152
+ installed: true,
1153
+ ...agentInfo,
1154
+ type: 'builtin',
1155
+ }, 201);
1156
+ }
640
1157
  }
641
1158
  catch (error) {
642
1159
  return next(error);
@@ -644,61 +1161,351 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
644
1161
  });
645
1162
  app.post('/api/agents/switch', express.json(), async (req, res, next) => {
646
1163
  try {
647
- const { name } = AgentNameSchema.parse(req.body);
648
- const result = await switchAgentByName(name);
1164
+ const { id, path } = parseBody(AgentIdentifierSchema, req.body);
1165
+ // Route based on presence of path parameter
1166
+ const result = path ? await switchAgentByPath(path) : await switchAgentById(id);
649
1167
  return sendJsonResponse(res, { switched: true, ...result });
650
1168
  }
651
1169
  catch (error) {
652
- if (error instanceof Error &&
653
- error.message &&
654
- error.message.includes('already in progress')) {
655
- return res.status(409).json({ error: error.message });
1170
+ return next(error);
1171
+ }
1172
+ });
1173
+ app.post('/api/agents/validate-name', express.json(), async (req, res, next) => {
1174
+ try {
1175
+ const { id } = parseBody(AgentIdentifierSchema, req.body);
1176
+ const agents = await Dexto.listAgents();
1177
+ // Check if name exists in installed agents
1178
+ const installedAgent = agents.installed.find((a) => a.id === id);
1179
+ if (installedAgent) {
1180
+ return sendJsonResponse(res, {
1181
+ valid: false,
1182
+ conflict: installedAgent.type,
1183
+ message: `Agent id '${id}' already exists (${installedAgent.type})`,
1184
+ });
656
1185
  }
1186
+ // Check if name exists in available agents (registry)
1187
+ const availableAgent = agents.available.find((a) => a.id === id);
1188
+ if (availableAgent) {
1189
+ return sendJsonResponse(res, {
1190
+ valid: false,
1191
+ conflict: availableAgent.type,
1192
+ message: `Agent id '${id}' conflicts with ${availableAgent.type} agent`,
1193
+ });
1194
+ }
1195
+ return sendJsonResponse(res, { valid: true });
1196
+ }
1197
+ catch (error) {
1198
+ return next(error);
1199
+ }
1200
+ });
1201
+ app.post('/api/agents/uninstall', express.json(), async (req, res, next) => {
1202
+ try {
1203
+ const { id, force } = parseBody(UninstallAgentSchema, req.body);
1204
+ await Dexto.uninstallAgent(id, force);
1205
+ return sendJsonResponse(res, { uninstalled: true, id });
1206
+ }
1207
+ catch (error) {
1208
+ return next(error);
1209
+ }
1210
+ });
1211
+ // Schema for creating custom agents via UI
1212
+ const CustomAgentCreateSchema = z
1213
+ .object({
1214
+ // Registry metadata
1215
+ id: z
1216
+ .string()
1217
+ .min(1, 'Agent ID is required')
1218
+ .regex(/^[a-z0-9-]+$/, 'Agent ID must contain only lowercase letters, numbers, and hyphens')
1219
+ .describe('Unique agent identifier'),
1220
+ name: z
1221
+ .string()
1222
+ .min(1, 'Agent name is required')
1223
+ .describe('Display name for the agent'),
1224
+ description: z
1225
+ .string()
1226
+ .min(1, 'Description is required')
1227
+ .describe('One-line description of the agent'),
1228
+ author: z.string().optional().describe('Author or organization'),
1229
+ tags: z.array(z.string()).default([]).describe('Tags for discovery'),
1230
+ // Agent configuration
1231
+ llm: z
1232
+ .object({
1233
+ provider: z.enum(LLM_PROVIDERS).describe('LLM provider id'),
1234
+ model: z.string().min(1, 'Model is required').describe('Model name'),
1235
+ apiKey: z
1236
+ .string()
1237
+ .optional()
1238
+ .describe('API key or environment variable reference (e.g., $OPENAI_API_KEY)'),
1239
+ })
1240
+ .strict()
1241
+ .describe('LLM configuration'),
1242
+ systemPrompt: z
1243
+ .string()
1244
+ .min(1, 'System prompt is required')
1245
+ .describe('System prompt for the agent'),
1246
+ })
1247
+ .strict();
1248
+ // Create a new custom agent from UI
1249
+ app.post('/api/agents/custom/create', express.json(), async (req, res, next) => {
1250
+ try {
1251
+ const { id, name, description, author, tags, llm, systemPrompt } = parseBody(CustomAgentCreateSchema, req.body);
1252
+ const provider = llm.provider;
1253
+ // Handle API key: if it's a raw key, store securely and use env var reference
1254
+ let apiKeyRef;
1255
+ if (llm.apiKey && !llm.apiKey.startsWith('$')) {
1256
+ // Raw API key provided - store securely and get env var reference
1257
+ const meta = await saveProviderApiKey(provider, llm.apiKey, process.cwd());
1258
+ apiKeyRef = `$${meta.envVar}`;
1259
+ logger.info(`Stored API key securely for ${provider}, using env var: ${meta.envVar}`);
1260
+ }
1261
+ else if (llm.apiKey) {
1262
+ // Already an env var reference
1263
+ apiKeyRef = llm.apiKey;
1264
+ }
1265
+ // Create agent YAML content (with env var reference instead of raw key)
1266
+ const agentConfig = {
1267
+ llm: {
1268
+ provider,
1269
+ model: llm.model,
1270
+ apiKey: apiKeyRef || `$${getPrimaryApiKeyEnvVar(provider)}`,
1271
+ },
1272
+ systemPrompt,
1273
+ };
1274
+ const yamlContent = yamlStringify(agentConfig);
1275
+ logger.info(`Creating agent config for ${id}:`, { agentConfig, yamlContent });
1276
+ // Create temporary file
1277
+ const tmpDir = os.tmpdir();
1278
+ const tmpFile = path.join(tmpDir, `${id}-${Date.now()}.yml`);
1279
+ await fs.writeFile(tmpFile, yamlContent, 'utf-8');
1280
+ try {
1281
+ // Install the custom agent
1282
+ await Dexto.installCustomAgent(id, tmpFile, {
1283
+ name,
1284
+ description,
1285
+ author: author || 'Custom',
1286
+ tags: tags || [],
1287
+ }, false // Don't inject preferences
1288
+ );
1289
+ // Clean up temp file
1290
+ await fs.unlink(tmpFile).catch(() => { });
1291
+ return sendJsonResponse(res, { created: true, id, name }, 201);
1292
+ }
1293
+ catch (installError) {
1294
+ // Clean up temp file on error
1295
+ await fs.unlink(tmpFile).catch(() => { });
1296
+ throw installError;
1297
+ }
1298
+ }
1299
+ catch (error) {
657
1300
  return next(error);
658
1301
  }
659
1302
  });
660
1303
  // Configuration export endpoint
661
- /**
662
- * Helper function to redact sensitive environment variables
663
- */
664
- function redactEnvValue(value) {
665
- if (value && typeof value === 'string' && value.length > 0) {
666
- return '[REDACTED]';
1304
+ // Get default greeting (for UI consumption)
1305
+ const GetGreetingQuerySchema = z.object({
1306
+ sessionId: z.string().optional(),
1307
+ });
1308
+ app.get('/api/greeting', async (req, res, next) => {
1309
+ try {
1310
+ ensureAgentAvailable();
1311
+ const { sessionId } = parseQuery(GetGreetingQuerySchema, req.query);
1312
+ const config = activeAgent.getEffectiveConfig(sessionId);
1313
+ res.json({ greeting: config.greeting });
667
1314
  }
668
- return value;
669
- }
670
- /**
671
- * Helper function to redact environment variables in a server config
672
- */
673
- function redactServerEnvVars(serverConfig) {
674
- if (!serverConfig.env) {
675
- return serverConfig;
1315
+ catch (error) {
1316
+ return next(error);
676
1317
  }
677
- const redactedEnv = {};
678
- for (const [key, value] of Object.entries(serverConfig.env)) {
679
- redactedEnv[key] = redactEnvValue(value);
1318
+ });
1319
+ // ============= AGENT CONFIGURATION MANAGEMENT =============
1320
+ // Get agent file path
1321
+ app.get('/api/agent/path', async (req, res, next) => {
1322
+ try {
1323
+ ensureAgentAvailable();
1324
+ const agentPath = activeAgent.getAgentFilePath();
1325
+ const relativePath = path.basename(agentPath);
1326
+ const ext = path.extname(agentPath);
1327
+ const name = path.basename(agentPath, ext);
1328
+ res.json({
1329
+ path: agentPath,
1330
+ relativePath,
1331
+ name,
1332
+ isDefault: name === 'default-agent',
1333
+ });
680
1334
  }
681
- return {
682
- ...serverConfig,
683
- env: redactedEnv,
684
- };
685
- }
686
- /**
687
- * Helper function to redact all MCP servers configuration
688
- */
689
- function redactMcpServersConfig(mcpServers) {
690
- if (!mcpServers) {
691
- return {};
1335
+ catch (error) {
1336
+ return next(error);
692
1337
  }
693
- const redactedServers = {};
694
- for (const [name, serverConfig] of Object.entries(mcpServers)) {
695
- redactedServers[name] = redactServerEnvVars(serverConfig);
1338
+ });
1339
+ // Get editable agent configuration (non-redacted YAML)
1340
+ app.get('/api/agent/config', async (req, res, next) => {
1341
+ try {
1342
+ ensureAgentAvailable();
1343
+ // Get the agent file path being used
1344
+ const agentPath = activeAgent.getAgentFilePath();
1345
+ // Read raw YAML from file (not expanded env vars)
1346
+ const yamlContent = await fs.readFile(agentPath, 'utf-8');
1347
+ // Get metadata
1348
+ const stats = await fs.stat(agentPath);
1349
+ res.json({
1350
+ yaml: yamlContent,
1351
+ path: agentPath,
1352
+ relativePath: path.basename(agentPath),
1353
+ lastModified: stats.mtime,
1354
+ warnings: [
1355
+ 'Environment variables ($VAR) will be resolved at runtime',
1356
+ 'API keys should use environment variables',
1357
+ ],
1358
+ });
696
1359
  }
697
- return redactedServers;
698
- }
699
- app.get('/api/config.yaml', async (req, res, next) => {
1360
+ catch (error) {
1361
+ return next(error);
1362
+ }
1363
+ });
1364
+ // Validate agent configuration without saving
1365
+ const AgentConfigValidateSchema = z.object({
1366
+ yaml: z.string().min(1, 'YAML content is required'),
1367
+ });
1368
+ app.post('/api/agent/validate', express.json(), async (req, res, next) => {
1369
+ try {
1370
+ ensureAgentAvailable();
1371
+ const { yaml } = parseBody(AgentConfigValidateSchema, req.body);
1372
+ // Parse YAML
1373
+ let parsed;
1374
+ try {
1375
+ parsed = yamlParse(yaml);
1376
+ }
1377
+ catch (parseError) {
1378
+ return res.json({
1379
+ valid: false,
1380
+ errors: [
1381
+ {
1382
+ line: parseError.linePos?.[0]?.line || 1,
1383
+ column: parseError.linePos?.[0]?.col || 1,
1384
+ message: parseError.message,
1385
+ code: 'YAML_PARSE_ERROR',
1386
+ },
1387
+ ],
1388
+ warnings: [],
1389
+ });
1390
+ }
1391
+ // Validate against schema
1392
+ const result = AgentConfigSchema.safeParse(parsed);
1393
+ if (!result.success) {
1394
+ const errors = result.error.errors.map((err) => ({
1395
+ path: err.path.join('.'),
1396
+ message: err.message,
1397
+ code: 'SCHEMA_VALIDATION_ERROR',
1398
+ }));
1399
+ return res.json({
1400
+ valid: false,
1401
+ errors,
1402
+ warnings: [],
1403
+ });
1404
+ }
1405
+ // Check for warnings (e.g., plain text API keys)
1406
+ const warnings = [];
1407
+ if (parsed.llm?.apiKey && !parsed.llm.apiKey.startsWith('$')) {
1408
+ warnings.push({
1409
+ path: 'llm.apiKey',
1410
+ message: 'Consider using environment variable instead of plain text',
1411
+ code: 'SECURITY_WARNING',
1412
+ });
1413
+ }
1414
+ res.json({
1415
+ valid: true,
1416
+ errors: [],
1417
+ warnings,
1418
+ });
1419
+ }
1420
+ catch (error) {
1421
+ return next(error);
1422
+ }
1423
+ });
1424
+ // Save agent configuration
1425
+ const AgentConfigSaveSchema = z.object({
1426
+ yaml: z.string().min(1, 'YAML content is required'),
1427
+ });
1428
+ app.post('/api/agent/config', express.json(), async (req, res, next) => {
1429
+ try {
1430
+ ensureAgentAvailable();
1431
+ const { yaml } = parseBody(AgentConfigSaveSchema, req.body);
1432
+ // Validate YAML syntax first
1433
+ let parsed;
1434
+ try {
1435
+ parsed = yamlParse(yaml);
1436
+ }
1437
+ catch (parseError) {
1438
+ throw new DextoValidationError([
1439
+ {
1440
+ code: AgentErrorCode.INVALID_CONFIG,
1441
+ message: `Invalid YAML syntax: ${parseError.message}`,
1442
+ scope: ErrorScope.AGENT,
1443
+ type: ErrorType.USER,
1444
+ severity: 'error',
1445
+ },
1446
+ ]);
1447
+ }
1448
+ // Validate schema
1449
+ const validationResult = AgentConfigSchema.safeParse(parsed);
1450
+ if (!validationResult.success) {
1451
+ throw new DextoValidationError(validationResult.error.errors.map((err) => ({
1452
+ code: AgentErrorCode.INVALID_CONFIG,
1453
+ message: `${err.path.join('.')}: ${err.message}`,
1454
+ scope: ErrorScope.AGENT,
1455
+ type: ErrorType.USER,
1456
+ severity: 'error',
1457
+ })));
1458
+ }
1459
+ // Get target file path
1460
+ const agentPath = activeAgent.getAgentFilePath();
1461
+ // Create backup
1462
+ const backupPath = `${agentPath}.backup`;
1463
+ await fs.copyFile(agentPath, backupPath);
1464
+ try {
1465
+ // Write new config
1466
+ await fs.writeFile(agentPath, yaml, 'utf-8');
1467
+ // Reload configuration to detect what changed
1468
+ const reloadResult = await activeAgent.reloadConfig();
1469
+ // If any changes require restart, automatically restart the agent
1470
+ if (reloadResult.restartRequired.length > 0) {
1471
+ logger.info(`Auto-restarting agent to apply changes: ${reloadResult.restartRequired.join(', ')}`);
1472
+ await activeAgent.restart();
1473
+ logger.info('Agent restarted successfully with all event subscribers reconnected');
1474
+ }
1475
+ // Clean up backup file after successful save
1476
+ await fs.unlink(backupPath).catch(() => {
1477
+ // Ignore errors if backup file doesn't exist
1478
+ });
1479
+ logger.info(`Agent configuration saved and applied: ${agentPath}`);
1480
+ res.json({
1481
+ ok: true,
1482
+ path: agentPath,
1483
+ reloaded: true,
1484
+ restarted: reloadResult.restartRequired.length > 0,
1485
+ changesApplied: reloadResult.restartRequired,
1486
+ message: reloadResult.restartRequired.length > 0
1487
+ ? 'Configuration saved and applied successfully (agent restarted)'
1488
+ : 'Configuration saved successfully (no changes detected)',
1489
+ });
1490
+ }
1491
+ catch (writeError) {
1492
+ // Restore backup on error
1493
+ await fs.copyFile(backupPath, agentPath);
1494
+ throw writeError;
1495
+ }
1496
+ }
1497
+ catch (error) {
1498
+ return next(error);
1499
+ }
1500
+ });
1501
+ // Export effective agent configuration (with masked secrets)
1502
+ const ExportConfigQuerySchema = z.object({
1503
+ sessionId: z.string().optional(),
1504
+ });
1505
+ app.get('/api/agent/config/export', async (req, res, next) => {
700
1506
  try {
701
- const sessionId = req.query.sessionId;
1507
+ ensureAgentAvailable();
1508
+ const { sessionId } = parseQuery(ExportConfigQuerySchema, req.query);
702
1509
  const config = activeAgent.getEffectiveConfig(sessionId);
703
1510
  // Export config as YAML, masking sensitive data
704
1511
  const maskedConfig = {
@@ -717,21 +1524,14 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
717
1524
  return next(error);
718
1525
  }
719
1526
  });
720
- // Get default greeting (for UI consumption)
721
- app.get('/api/greeting', async (req, res, next) => {
722
- try {
723
- const sessionId = req.query.sessionId;
724
- const config = activeAgent.getEffectiveConfig(sessionId);
725
- res.json({ greeting: config.greeting });
726
- }
727
- catch (error) {
728
- return next(error);
729
- }
730
- });
1527
+ // ============= LLM MANAGEMENT =============
731
1528
  // Get current LLM configuration
1529
+ const GetCurrentLLMQuerySchema = z.object({
1530
+ sessionId: z.string().optional(),
1531
+ });
732
1532
  app.get('/api/llm/current', async (req, res, next) => {
733
1533
  try {
734
- const { sessionId } = req.query;
1534
+ const { sessionId } = parseQuery(GetCurrentLLMQuerySchema, req.query);
735
1535
  // Use session-specific config if sessionId is provided, otherwise use default
736
1536
  const currentConfig = sessionId
737
1537
  ? activeAgent.getEffectiveConfig(sessionId).llm
@@ -751,37 +1551,35 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
751
1551
  return next(error);
752
1552
  }
753
1553
  });
754
- // (Deprecated) /api/llm/providers has been replaced by /api/llm/catalog
755
1554
  // LLM Catalog: providers, models, and API key presence (with filters)
1555
+ const LLMCatalogQuerySchema = z.object({
1556
+ provider: z
1557
+ .union([z.string(), z.array(z.string())])
1558
+ .optional()
1559
+ .transform((value) => Array.isArray(value) ? value : value ? value.split(',') : undefined),
1560
+ hasKey: z
1561
+ .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
1562
+ .optional()
1563
+ .transform((raw) => raw === 'true' || raw === '1'
1564
+ ? true
1565
+ : raw === 'false' || raw === '0'
1566
+ ? false
1567
+ : undefined),
1568
+ router: z.enum(LLM_ROUTERS).optional(),
1569
+ fileType: z.enum(SUPPORTED_FILE_TYPES).optional(),
1570
+ defaultOnly: z
1571
+ .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
1572
+ .optional()
1573
+ .transform((raw) => raw === 'true' || raw === '1'
1574
+ ? true
1575
+ : raw === 'false' || raw === '0'
1576
+ ? false
1577
+ : undefined),
1578
+ mode: z.enum(['grouped', 'flat']).default('grouped'),
1579
+ });
756
1580
  app.get('/api/llm/catalog', async (req, res, next) => {
757
1581
  try {
758
- // Parse query parameters with Zod
759
- const QuerySchema = z.object({
760
- provider: z
761
- .union([z.string(), z.array(z.string())])
762
- .optional()
763
- .transform((value) => Array.isArray(value) ? value : value ? value.split(',') : undefined),
764
- hasKey: z
765
- .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
766
- .optional()
767
- .transform((raw) => raw === 'true' || raw === '1'
768
- ? true
769
- : raw === 'false' || raw === '0'
770
- ? false
771
- : undefined),
772
- router: z.enum(LLM_ROUTERS).optional(),
773
- fileType: z.enum(SUPPORTED_FILE_TYPES).optional(),
774
- defaultOnly: z
775
- .union([z.literal('true'), z.literal('false'), z.literal('1'), z.literal('0')])
776
- .optional()
777
- .transform((raw) => raw === 'true' || raw === '1'
778
- ? true
779
- : raw === 'false' || raw === '0'
780
- ? false
781
- : undefined),
782
- mode: z.enum(['grouped', 'flat']).optional().default('grouped'),
783
- });
784
- const queryParams = QuerySchema.parse(req.query);
1582
+ const queryParams = LLMCatalogQuerySchema.parse(req.query);
785
1583
  const providers = {};
786
1584
  for (const provider of LLM_PROVIDERS) {
787
1585
  const info = LLM_REGISTRY[provider];
@@ -876,13 +1674,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
876
1674
  }
877
1675
  });
878
1676
  // Save provider API key (never echoes the key back)
1677
+ const SaveProviderApiKeyBodySchema = z.object({
1678
+ provider: z.enum(LLM_PROVIDERS),
1679
+ apiKey: z.string().min(1, 'API key is required'),
1680
+ });
879
1681
  app.post('/api/llm/key', express.json({ limit: '4kb' }), async (req, res, next) => {
880
1682
  try {
881
- const schema = z.object({
882
- provider: z.enum(LLM_PROVIDERS),
883
- apiKey: z.string().min(1, 'API key is required'),
884
- });
885
- const body = schema.parse(req.body);
1683
+ const body = parseBody(SaveProviderApiKeyBodySchema, req.body);
886
1684
  const meta = await saveProviderApiKey(body.provider, body.apiKey, process.cwd());
887
1685
  return sendJsonResponse(res, { ok: true, provider: body.provider, envVar: meta.envVar }, 200);
888
1686
  }
@@ -891,11 +1689,15 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
891
1689
  }
892
1690
  });
893
1691
  // Switch LLM configuration
1692
+ const SwitchLLMBodySchema = z
1693
+ .object({
1694
+ sessionId: z.string().optional(),
1695
+ })
1696
+ .passthrough(); // Allow additional LLM config fields
894
1697
  app.post('/api/llm/switch', express.json(), async (req, res, next) => {
895
1698
  try {
896
- const body = (req.body ?? {});
897
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined;
898
- const { sessionId: _omit, ...llmCandidate } = body;
1699
+ const parsed = parseBody(SwitchLLMBodySchema, req.body);
1700
+ const { sessionId, ...llmCandidate } = parsed;
899
1701
  const llmConfig = LLMUpdatesSchema.parse(llmCandidate);
900
1702
  const config = await activeAgent.switchLLM(llmConfig, sessionId);
901
1703
  return res.status(200).json({ config, sessionId });
@@ -906,7 +1708,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
906
1708
  });
907
1709
  // Session Management APIs
908
1710
  // List all active sessions
909
- app.get('/api/sessions', async (req, res, next) => {
1711
+ app.get('/api/sessions', async (_req, res, next) => {
910
1712
  try {
911
1713
  const sessionIds = await activeAgent.listSessions();
912
1714
  const sessions = await Promise.all(sessionIds.map(async (id) => {
@@ -917,6 +1719,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
917
1719
  createdAt: metadata?.createdAt || null,
918
1720
  lastActivity: metadata?.lastActivity || null,
919
1721
  messageCount: metadata?.messageCount || 0,
1722
+ title: metadata?.title || null,
920
1723
  };
921
1724
  }
922
1725
  catch (_error) {
@@ -926,6 +1729,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
926
1729
  createdAt: null,
927
1730
  lastActivity: null,
928
1731
  messageCount: 0,
1732
+ title: null,
929
1733
  };
930
1734
  }
931
1735
  }));
@@ -936,9 +1740,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
936
1740
  }
937
1741
  });
938
1742
  // Create a new session
1743
+ const CreateSessionBodySchema = z.object({
1744
+ sessionId: z.string().optional(),
1745
+ });
939
1746
  app.post('/api/sessions', express.json(), async (req, res, next) => {
940
1747
  try {
941
- const { sessionId } = req.body;
1748
+ const { sessionId } = parseBody(CreateSessionBodySchema, req.body);
942
1749
  const session = await activeAgent.createSession(sessionId);
943
1750
  const metadata = await activeAgent.getSessionMetadata(session.id);
944
1751
  return res.status(201).json({
@@ -947,6 +1754,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
947
1754
  createdAt: metadata?.createdAt || Date.now(),
948
1755
  lastActivity: metadata?.lastActivity || Date.now(),
949
1756
  messageCount: metadata?.messageCount || 0,
1757
+ title: metadata?.title || null,
950
1758
  },
951
1759
  });
952
1760
  }
@@ -955,7 +1763,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
955
1763
  }
956
1764
  });
957
1765
  // Get current working session (must come before parameterized route)
958
- app.get('/api/sessions/current', async (req, res, next) => {
1766
+ app.get('/api/sessions/current', async (_req, res, next) => {
959
1767
  try {
960
1768
  const currentSessionId = activeAgent.getCurrentSessionId();
961
1769
  return res.json({ currentSessionId });
@@ -965,9 +1773,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
965
1773
  }
966
1774
  });
967
1775
  // Get session details
1776
+ const GetSessionDetailsParamsSchema = z.object({
1777
+ sessionId: z.string().min(1, 'Session ID is required'),
1778
+ });
968
1779
  app.get('/api/sessions/:sessionId', async (req, res, next) => {
969
1780
  try {
970
- const { sessionId } = req.params;
1781
+ const { sessionId } = parseQuery(GetSessionDetailsParamsSchema, req.params);
971
1782
  const metadata = await activeAgent.getSessionMetadata(sessionId);
972
1783
  const history = await activeAgent.getSessionHistory(sessionId);
973
1784
  return res.json({
@@ -976,6 +1787,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
976
1787
  createdAt: metadata?.createdAt || null,
977
1788
  lastActivity: metadata?.lastActivity || null,
978
1789
  messageCount: metadata?.messageCount || 0,
1790
+ title: metadata?.title || null,
979
1791
  history: history.length,
980
1792
  },
981
1793
  });
@@ -985,9 +1797,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
985
1797
  }
986
1798
  });
987
1799
  // Get session conversation history
1800
+ const GetSessionHistoryParamsSchema = z.object({
1801
+ sessionId: z.string().min(1, 'Session ID is required'),
1802
+ });
988
1803
  app.get('/api/sessions/:sessionId/history', async (req, res, next) => {
989
1804
  try {
990
- const { sessionId } = req.params;
1805
+ const { sessionId } = parseQuery(GetSessionHistoryParamsSchema, req.params);
991
1806
  // getSessionHistory already checks existence via getSession
992
1807
  const history = await activeAgent.getSessionHistory(sessionId);
993
1808
  return res.json({ history });
@@ -997,6 +1812,13 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
997
1812
  }
998
1813
  });
999
1814
  // Search messages across all sessions or within a specific session
1815
+ const SearchQuerySchema = z.object({
1816
+ q: z.string().min(1, 'Search query is required'),
1817
+ limit: z.coerce.number().min(1).max(100).optional(),
1818
+ offset: z.coerce.number().min(0).optional(),
1819
+ sessionId: z.string().optional(),
1820
+ role: z.enum(['user', 'assistant', 'system', 'tool']).optional(),
1821
+ });
1000
1822
  app.get('/api/search/messages', async (req, res, next) => {
1001
1823
  try {
1002
1824
  const { q: query, limit, offset, sessionId, role, } = parseQuery(SearchQuerySchema, req.query);
@@ -1014,9 +1836,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1014
1836
  }
1015
1837
  });
1016
1838
  // Search sessions that contain the query
1839
+ const SearchSessionsQuerySchema = z.object({
1840
+ q: z.string().min(1, 'Search query is required'),
1841
+ });
1017
1842
  app.get('/api/search/sessions', async (req, res, next) => {
1018
1843
  try {
1019
- const { q: query } = parseQuery(z.object({ q: z.string().min(1, 'Search query is required') }), req.query);
1844
+ const { q: query } = parseQuery(SearchSessionsQuerySchema, req.query);
1020
1845
  const searchResults = await activeAgent.searchSessions(query);
1021
1846
  return sendJsonResponse(res, searchResults);
1022
1847
  }
@@ -1025,9 +1850,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1025
1850
  }
1026
1851
  });
1027
1852
  // Delete a session
1853
+ const DeleteSessionParamsSchema = z.object({
1854
+ sessionId: z.string().min(1, 'Session ID is required'),
1855
+ });
1028
1856
  app.delete('/api/sessions/:sessionId', async (req, res, next) => {
1029
1857
  try {
1030
- const { sessionId } = req.params;
1858
+ const { sessionId } = parseQuery(DeleteSessionParamsSchema, req.params);
1031
1859
  // deleteSession already checks existence internally
1032
1860
  await activeAgent.deleteSession(sessionId);
1033
1861
  return res.json({ status: 'deleted', sessionId });
@@ -1036,10 +1864,40 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1036
1864
  return next(error);
1037
1865
  }
1038
1866
  });
1867
+ // Rename session title
1868
+ const PatchSessionBodySchema = z.object({
1869
+ title: z.string().min(1, 'Title is required').max(120, 'Title too long'),
1870
+ });
1871
+ const PatchSessionParamsSchema = z.object({
1872
+ sessionId: z.string().min(1, 'Session ID is required'),
1873
+ });
1874
+ app.patch('/api/sessions/:sessionId', express.json(), async (req, res, next) => {
1875
+ try {
1876
+ const { sessionId } = parseQuery(PatchSessionParamsSchema, req.params);
1877
+ const { title } = parseBody(PatchSessionBodySchema, req.body);
1878
+ await activeAgent.setSessionTitle(sessionId, title);
1879
+ const metadata = await activeAgent.getSessionMetadata(sessionId);
1880
+ return res.json({
1881
+ session: {
1882
+ id: sessionId,
1883
+ createdAt: metadata?.createdAt || null,
1884
+ lastActivity: metadata?.lastActivity || null,
1885
+ messageCount: metadata?.messageCount || 0,
1886
+ title: metadata?.title || title,
1887
+ },
1888
+ });
1889
+ }
1890
+ catch (error) {
1891
+ return next(error);
1892
+ }
1893
+ });
1039
1894
  // Load session as current working session and set as default
1895
+ const LoadSessionParamsSchema = z.object({
1896
+ sessionId: z.string().min(1, 'Session ID is required'),
1897
+ });
1040
1898
  app.post('/api/sessions/:sessionId/load', async (req, res, next) => {
1041
1899
  try {
1042
- const { sessionId } = req.params;
1900
+ const { sessionId } = parseQuery(LoadSessionParamsSchema, req.params);
1043
1901
  // Handle null/reset case
1044
1902
  if (sessionId === 'null' || sessionId === 'undefined') {
1045
1903
  await activeAgent.loadSessionAsDefault(null);
@@ -1064,6 +1922,11 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1064
1922
  });
1065
1923
  // Webhook Management APIs
1066
1924
  // Register a new webhook endpoint
1925
+ const WebhookRequestSchema = z.object({
1926
+ url: z.string().url('Invalid URL format'),
1927
+ secret: z.string().optional(),
1928
+ description: z.string().optional(),
1929
+ });
1067
1930
  app.post('/api/webhooks', express.json(), async (req, res, next) => {
1068
1931
  try {
1069
1932
  const { url, secret, description } = parseBody(WebhookRequestSchema, req.body);
@@ -1092,7 +1955,7 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1092
1955
  }
1093
1956
  });
1094
1957
  // List all registered webhooks
1095
- app.get('/api/webhooks', async (req, res, next) => {
1958
+ app.get('/api/webhooks', async (_req, res, next) => {
1096
1959
  try {
1097
1960
  const webhooks = webhookSubscriber.getWebhooks().map((webhook) => ({
1098
1961
  id: webhook.id,
@@ -1107,9 +1970,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1107
1970
  }
1108
1971
  });
1109
1972
  // Get a specific webhook
1973
+ const GetWebhookParamsSchema = z.object({
1974
+ webhookId: z.string().min(1, 'Webhook ID is required'),
1975
+ });
1110
1976
  app.get('/api/webhooks/:webhookId', async (req, res, next) => {
1111
1977
  try {
1112
- const { webhookId } = req.params;
1978
+ const { webhookId } = parseQuery(GetWebhookParamsSchema, req.params);
1113
1979
  const webhook = webhookSubscriber.getWebhook(webhookId);
1114
1980
  if (!webhook) {
1115
1981
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1128,9 +1994,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1128
1994
  }
1129
1995
  });
1130
1996
  // Remove a webhook endpoint
1997
+ const DeleteWebhookParamsSchema = z.object({
1998
+ webhookId: z.string().min(1, 'Webhook ID is required'),
1999
+ });
1131
2000
  app.delete('/api/webhooks/:webhookId', async (req, res, next) => {
1132
2001
  try {
1133
- const { webhookId } = req.params;
2002
+ const { webhookId } = parseQuery(DeleteWebhookParamsSchema, req.params);
1134
2003
  const removed = webhookSubscriber.removeWebhook(webhookId);
1135
2004
  if (!removed) {
1136
2005
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1143,9 +2012,12 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1143
2012
  }
1144
2013
  });
1145
2014
  // Test a webhook endpoint
2015
+ const TestWebhookParamsSchema = z.object({
2016
+ webhookId: z.string().min(1, 'Webhook ID is required'),
2017
+ });
1146
2018
  app.post('/api/webhooks/:webhookId/test', async (req, res, next) => {
1147
2019
  try {
1148
- const { webhookId } = req.params;
2020
+ const { webhookId } = parseQuery(TestWebhookParamsSchema, req.params);
1149
2021
  const webhook = webhookSubscriber.getWebhook(webhookId);
1150
2022
  if (!webhook) {
1151
2023
  return res.status(404).json({ error: 'Webhook not found' });
@@ -1170,8 +2042,8 @@ export async function initializeApi(agent, agentCardOverride, listenPort, agentN
1170
2042
  app.use(errorHandler);
1171
2043
  return { app, server, wss, webSubscriber, webhookSubscriber };
1172
2044
  }
1173
- export async function startApiServer(agent, port = 3000, agentCardOverride, agentName) {
1174
- const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port, agentName);
2045
+ export async function startApiServer(agent, port = 3000, agentCardOverride, agentId) {
2046
+ const { server, wss, webSubscriber, webhookSubscriber } = await initializeApi(agent, agentCardOverride, port, agentId);
1175
2047
  // API server for REST endpoints and WebSocket connections
1176
2048
  server.listen(port, '0.0.0.0', () => {
1177
2049
  const networkInterfaces = os.networkInterfaces();