@lobu/gateway 2.8.0 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/agent-config-routes.test.ts +254 -0
  3. package/src/__tests__/agent-history-routes.test.ts +72 -0
  4. package/src/__tests__/agent-routes.test.ts +68 -0
  5. package/src/__tests__/agent-schedules-routes.test.ts +59 -0
  6. package/src/__tests__/agent-settings-store.test.ts +323 -0
  7. package/src/__tests__/chat-instance-manager-slack.test.ts +204 -0
  8. package/src/__tests__/chat-response-bridge.test.ts +131 -0
  9. package/src/__tests__/config-memory-plugins.test.ts +92 -0
  10. package/src/__tests__/config-request-store.test.ts +127 -0
  11. package/src/__tests__/connection-routes.test.ts +144 -0
  12. package/src/__tests__/core-services-store-selection.test.ts +92 -0
  13. package/src/__tests__/docker-deployment.test.ts +1211 -0
  14. package/src/__tests__/embedded-deployment.test.ts +342 -0
  15. package/src/__tests__/grant-store.test.ts +148 -0
  16. package/src/__tests__/http-proxy.test.ts +281 -0
  17. package/src/__tests__/instruction-service.test.ts +37 -0
  18. package/src/__tests__/link-buttons.test.ts +112 -0
  19. package/src/__tests__/lobu.test.ts +32 -0
  20. package/src/__tests__/mcp-config-service.test.ts +347 -0
  21. package/src/__tests__/mcp-proxy.test.ts +696 -0
  22. package/src/__tests__/message-handler-bridge.test.ts +17 -0
  23. package/src/__tests__/model-selection.test.ts +172 -0
  24. package/src/__tests__/oauth-templates.test.ts +39 -0
  25. package/src/__tests__/platform-adapter-slack-send.test.ts +114 -0
  26. package/src/__tests__/platform-helpers-model-resolution.test.ts +253 -0
  27. package/src/__tests__/provider-inheritance.test.ts +212 -0
  28. package/src/__tests__/routes/cli-auth.test.ts +337 -0
  29. package/src/__tests__/routes/interactions.test.ts +121 -0
  30. package/src/__tests__/secret-proxy.test.ts +85 -0
  31. package/src/__tests__/session-manager.test.ts +572 -0
  32. package/src/__tests__/setup.ts +133 -0
  33. package/src/__tests__/skill-and-mcp-registry.test.ts +203 -0
  34. package/src/__tests__/slack-routes.test.ts +161 -0
  35. package/src/__tests__/system-config-resolver.test.ts +75 -0
  36. package/src/__tests__/system-message-limiter.test.ts +89 -0
  37. package/src/__tests__/system-skills-service.test.ts +362 -0
  38. package/src/__tests__/transcription-service.test.ts +222 -0
  39. package/src/__tests__/utils/rate-limiter.test.ts +102 -0
  40. package/src/__tests__/worker-connection-manager.test.ts +497 -0
  41. package/src/__tests__/worker-job-router.test.ts +722 -0
  42. package/src/api/index.ts +1 -0
  43. package/src/api/platform.ts +292 -0
  44. package/src/api/response-renderer.ts +157 -0
  45. package/src/auth/agent-metadata-store.ts +168 -0
  46. package/src/auth/api-auth-middleware.ts +69 -0
  47. package/src/auth/api-key-provider-module.ts +213 -0
  48. package/src/auth/base-provider-module.ts +201 -0
  49. package/src/auth/chatgpt/chatgpt-oauth-module.ts +185 -0
  50. package/src/auth/chatgpt/device-code-client.ts +218 -0
  51. package/src/auth/chatgpt/index.ts +1 -0
  52. package/src/auth/claude/oauth-module.ts +280 -0
  53. package/src/auth/cli/token-service.ts +249 -0
  54. package/src/auth/external/client.ts +560 -0
  55. package/src/auth/external/device-code-client.ts +225 -0
  56. package/src/auth/mcp/config-service.ts +392 -0
  57. package/src/auth/mcp/proxy.ts +1088 -0
  58. package/src/auth/mcp/string-substitution.ts +17 -0
  59. package/src/auth/mcp/tool-cache.ts +90 -0
  60. package/src/auth/oauth/base-client.ts +267 -0
  61. package/src/auth/oauth/client.ts +153 -0
  62. package/src/auth/oauth/credentials.ts +7 -0
  63. package/src/auth/oauth/providers.ts +69 -0
  64. package/src/auth/oauth/state-store.ts +150 -0
  65. package/src/auth/oauth-templates.ts +179 -0
  66. package/src/auth/provider-catalog.ts +220 -0
  67. package/src/auth/provider-model-options.ts +41 -0
  68. package/src/auth/settings/agent-settings-store.ts +565 -0
  69. package/src/auth/settings/auth-profiles-manager.ts +216 -0
  70. package/src/auth/settings/index.ts +12 -0
  71. package/src/auth/settings/model-preference-store.ts +52 -0
  72. package/src/auth/settings/model-selection.ts +135 -0
  73. package/src/auth/settings/resolved-settings-view.ts +298 -0
  74. package/src/auth/settings/template-utils.ts +44 -0
  75. package/src/auth/settings/token-service.ts +88 -0
  76. package/src/auth/system-env-store.ts +98 -0
  77. package/src/auth/user-agents-store.ts +68 -0
  78. package/src/channels/binding-service.ts +214 -0
  79. package/src/channels/index.ts +4 -0
  80. package/src/cli/gateway.ts +1304 -0
  81. package/src/cli/index.ts +74 -0
  82. package/src/commands/built-in-commands.ts +80 -0
  83. package/src/commands/command-dispatcher.ts +94 -0
  84. package/src/commands/command-reply-adapters.ts +27 -0
  85. package/src/config/file-loader.ts +618 -0
  86. package/src/config/index.ts +588 -0
  87. package/src/config/network-allowlist.ts +71 -0
  88. package/src/connections/chat-instance-manager.ts +1284 -0
  89. package/src/connections/chat-response-bridge.ts +618 -0
  90. package/src/connections/index.ts +7 -0
  91. package/src/connections/interaction-bridge.ts +831 -0
  92. package/src/connections/message-handler-bridge.ts +415 -0
  93. package/src/connections/platform-auth-methods.ts +15 -0
  94. package/src/connections/types.ts +84 -0
  95. package/src/gateway/connection-manager.ts +291 -0
  96. package/src/gateway/index.ts +700 -0
  97. package/src/gateway/job-router.ts +201 -0
  98. package/src/gateway-main.ts +200 -0
  99. package/src/index.ts +41 -0
  100. package/src/infrastructure/queue/index.ts +12 -0
  101. package/src/infrastructure/queue/queue-producer.ts +148 -0
  102. package/src/infrastructure/queue/redis-queue.ts +361 -0
  103. package/src/infrastructure/queue/types.ts +133 -0
  104. package/src/infrastructure/redis/system-message-limiter.ts +94 -0
  105. package/src/interactions/config-request-store.ts +198 -0
  106. package/src/interactions.ts +363 -0
  107. package/src/lobu.ts +311 -0
  108. package/src/metrics/prometheus.ts +159 -0
  109. package/src/modules/module-system.ts +179 -0
  110. package/src/orchestration/base-deployment-manager.ts +900 -0
  111. package/src/orchestration/deployment-utils.ts +98 -0
  112. package/src/orchestration/impl/docker-deployment.ts +620 -0
  113. package/src/orchestration/impl/embedded-deployment.ts +268 -0
  114. package/src/orchestration/impl/index.ts +8 -0
  115. package/src/orchestration/impl/k8s/deployment.ts +1061 -0
  116. package/src/orchestration/impl/k8s/helpers.ts +610 -0
  117. package/src/orchestration/impl/k8s/index.ts +1 -0
  118. package/src/orchestration/index.ts +333 -0
  119. package/src/orchestration/message-consumer.ts +584 -0
  120. package/src/orchestration/scheduled-wakeup.ts +704 -0
  121. package/src/permissions/approval-policy.ts +36 -0
  122. package/src/permissions/grant-store.ts +219 -0
  123. package/src/platform/file-handler.ts +66 -0
  124. package/src/platform/link-buttons.ts +57 -0
  125. package/src/platform/renderer-utils.ts +44 -0
  126. package/src/platform/response-renderer.ts +84 -0
  127. package/src/platform/unified-thread-consumer.ts +187 -0
  128. package/src/platform.ts +318 -0
  129. package/src/proxy/http-proxy.ts +752 -0
  130. package/src/proxy/proxy-manager.ts +81 -0
  131. package/src/proxy/secret-proxy.ts +402 -0
  132. package/src/proxy/token-refresh-job.ts +143 -0
  133. package/src/routes/internal/audio.ts +141 -0
  134. package/src/routes/internal/device-auth.ts +566 -0
  135. package/src/routes/internal/files.ts +226 -0
  136. package/src/routes/internal/history.ts +69 -0
  137. package/src/routes/internal/images.ts +127 -0
  138. package/src/routes/internal/interactions.ts +84 -0
  139. package/src/routes/internal/middleware.ts +23 -0
  140. package/src/routes/internal/schedule.ts +226 -0
  141. package/src/routes/internal/types.ts +22 -0
  142. package/src/routes/openapi-auto.ts +239 -0
  143. package/src/routes/public/agent-access.ts +23 -0
  144. package/src/routes/public/agent-config.ts +675 -0
  145. package/src/routes/public/agent-history.ts +422 -0
  146. package/src/routes/public/agent-schedules.ts +296 -0
  147. package/src/routes/public/agent.ts +1086 -0
  148. package/src/routes/public/agents.ts +373 -0
  149. package/src/routes/public/channels.ts +191 -0
  150. package/src/routes/public/cli-auth.ts +883 -0
  151. package/src/routes/public/connections.ts +574 -0
  152. package/src/routes/public/landing.ts +16 -0
  153. package/src/routes/public/oauth.ts +147 -0
  154. package/src/routes/public/settings-auth.ts +104 -0
  155. package/src/routes/public/slack.ts +173 -0
  156. package/src/routes/shared/agent-ownership.ts +101 -0
  157. package/src/routes/shared/token-verifier.ts +34 -0
  158. package/src/services/core-services.ts +1053 -0
  159. package/src/services/image-generation-service.ts +257 -0
  160. package/src/services/instruction-service.ts +318 -0
  161. package/src/services/mcp-registry.ts +94 -0
  162. package/src/services/platform-helpers.ts +287 -0
  163. package/src/services/session-manager.ts +262 -0
  164. package/src/services/settings-resolver.ts +74 -0
  165. package/src/services/system-config-resolver.ts +90 -0
  166. package/src/services/system-skills-service.ts +229 -0
  167. package/src/services/transcription-service.ts +684 -0
  168. package/src/session.ts +110 -0
  169. package/src/spaces/index.ts +1 -0
  170. package/src/spaces/space-resolver.ts +17 -0
  171. package/src/stores/in-memory-agent-store.ts +403 -0
  172. package/src/stores/redis-agent-store.ts +279 -0
  173. package/src/utils/public-url.ts +44 -0
  174. package/src/utils/rate-limiter.ts +94 -0
  175. package/tsconfig.json +33 -0
@@ -0,0 +1,1086 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
3
+ import {
4
+ type AgentConfigStore,
5
+ createLogger,
6
+ createRootSpan,
7
+ findTemplateAgentId,
8
+ generateWorkerToken,
9
+ type InstalledProvider,
10
+ type McpServerConfig,
11
+ type NetworkConfig,
12
+ verifyWorkerToken,
13
+ } from "@lobu/core";
14
+ import { streamSSE } from "hono/streaming";
15
+ import { z } from "zod";
16
+ import {
17
+ createApiAuthMiddleware,
18
+ TOKEN_EXPIRATION_MS,
19
+ } from "../../auth/api-auth-middleware";
20
+ import type { CliTokenService } from "../../auth/cli/token-service";
21
+ import type { ExternalAuthClient } from "../../auth/external/client";
22
+ import type { AgentSettingsStore } from "../../auth/settings/agent-settings-store";
23
+ import type { QueueProducer } from "../../infrastructure/queue/queue-producer";
24
+ import { getModelProviderModules } from "../../modules/module-system";
25
+ import type { PlatformRegistry } from "../../platform";
26
+ import { resolveAgentOptions } from "../../services/platform-helpers";
27
+ import type { ISessionManager, ThreadSession } from "../../session";
28
+
29
+ const logger = createLogger("agent-api");
30
+
31
+ // =============================================================================
32
+ // Constants
33
+ // =============================================================================
34
+
35
+ const MAX_CONNECTIONS_PER_AGENT = 5;
36
+ const MAX_TOTAL_CONNECTIONS = 1000;
37
+
38
+ // SSE connection tracking
39
+ const sseConnections = new Map<string, Set<any>>();
40
+
41
+ // =============================================================================
42
+ // Zod Schemas
43
+ // =============================================================================
44
+
45
+ const NetworkConfigSchema = z.object({
46
+ allowedDomains: z.array(z.string()).optional(),
47
+ deniedDomains: z.array(z.string()).optional(),
48
+ });
49
+
50
+ const McpServerConfigSchema = z.object({
51
+ url: z.string().optional(),
52
+ type: z.enum(["sse", "stdio"]).optional(),
53
+ command: z.string().optional(),
54
+ args: z.array(z.string()).optional(),
55
+ env: z.record(z.string(), z.string()).optional(),
56
+ headers: z.record(z.string(), z.string()).optional(),
57
+ description: z.string().optional(),
58
+ });
59
+
60
+ const NixConfigSchema = z.object({
61
+ flakeUrl: z.string().optional(),
62
+ packages: z.array(z.string()).optional(),
63
+ });
64
+
65
+ const CreateAgentRequestSchema = z.object({
66
+ provider: z.string().default("claude").optional(),
67
+ model: z.string().optional(),
68
+ agentId: z.string().min(1).optional(),
69
+ userId: z.string().min(1).optional(),
70
+ thread: z.string().optional(),
71
+ forceNew: z.boolean().optional(),
72
+ dryRun: z.boolean().optional(),
73
+ networkConfig: NetworkConfigSchema.optional(),
74
+ mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
75
+ nix: NixConfigSchema.optional(),
76
+ });
77
+
78
+ const CreateAgentResponseSchema = z.object({
79
+ success: z.boolean(),
80
+ agentId: z.string(),
81
+ token: z.string(),
82
+ expiresAt: z.number(),
83
+ sseUrl: z.string(),
84
+ messagesUrl: z.string(),
85
+ });
86
+
87
+ const SlackRoutingInfoSchema = z.object({
88
+ channel: z.string().describe("Slack channel ID"),
89
+ thread: z.string().optional().describe("Thread timestamp for replies"),
90
+ team: z.string().optional().describe("Slack team ID"),
91
+ });
92
+
93
+ const SendMessageRequestSchema = z
94
+ .object({
95
+ content: z.string().optional().describe("Message content"),
96
+ message: z
97
+ .string()
98
+ .optional()
99
+ .describe("Message content (alias for content)"),
100
+ messageId: z.string().optional(),
101
+ platform: z
102
+ .string()
103
+ .optional()
104
+ .describe("Target platform (api, slack, telegram)"),
105
+ slack: SlackRoutingInfoSchema.optional().describe(
106
+ "Slack-specific routing info (required when platform=slack)"
107
+ ),
108
+ })
109
+ .passthrough();
110
+
111
+ const SendMessageResponseSchema = z.object({
112
+ success: z.boolean(),
113
+ messageId: z.string(),
114
+ agentId: z.string().optional(),
115
+ jobId: z.string().optional(),
116
+ eventsUrl: z.string().optional(),
117
+ queued: z.boolean(),
118
+ traceparent: z.string().optional(),
119
+ });
120
+
121
+ const AgentStatusResponseSchema = z.object({
122
+ success: z.boolean(),
123
+ agent: z.object({
124
+ agentId: z.string(),
125
+ userId: z.string(),
126
+ status: z.string(),
127
+ createdAt: z.number(),
128
+ lastActivity: z.number(),
129
+ hasActiveConnection: z.boolean(),
130
+ }),
131
+ });
132
+
133
+ const ErrorResponseSchema = z.object({
134
+ success: z.boolean(),
135
+ error: z.string(),
136
+ details: z.string().optional(),
137
+ });
138
+
139
+ const SuccessResponseSchema = z.object({
140
+ success: z.boolean(),
141
+ message: z.string().optional(),
142
+ agentId: z.string().optional(),
143
+ });
144
+
145
+ // Path parameters
146
+ const AgentIdParamSchema = z.object({
147
+ agentId: z.string(),
148
+ });
149
+
150
+ // =============================================================================
151
+ // Validation Helpers
152
+ // =============================================================================
153
+
154
+ function validateDomainPattern(pattern: string): string | null {
155
+ if (!pattern || typeof pattern !== "string") {
156
+ return "Domain pattern must be a non-empty string";
157
+ }
158
+ const trimmed = pattern.trim().toLowerCase();
159
+ if (trimmed === "*") return "Bare wildcard '*' is not allowed";
160
+ if (trimmed.includes("://"))
161
+ return `Domain pattern cannot contain protocol: ${pattern}`;
162
+ if (trimmed.includes("/"))
163
+ return `Domain pattern cannot contain path: ${pattern}`;
164
+ if (trimmed.includes(":") && !trimmed.includes("[")) {
165
+ return `Domain pattern cannot contain port: ${pattern}`;
166
+ }
167
+ if (trimmed.startsWith("*.") || trimmed.startsWith(".")) {
168
+ const domain = trimmed.startsWith("*.")
169
+ ? trimmed.substring(2)
170
+ : trimmed.substring(1);
171
+ if (!domain.includes(".")) {
172
+ return `Wildcard pattern too broad: ${pattern}`;
173
+ }
174
+ } else if (!trimmed.includes(".")) {
175
+ return `Invalid domain pattern: ${pattern}`;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function validateNetworkConfig(config: NetworkConfig): string | null {
181
+ for (const domains of [config.allowedDomains, config.deniedDomains]) {
182
+ if (domains) {
183
+ for (const domain of domains) {
184
+ const error = validateDomainPattern(domain);
185
+ if (error) return error;
186
+ }
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+
192
+ function validateMcpServerConfig(
193
+ id: string,
194
+ config: McpServerConfig
195
+ ): string | null {
196
+ if (!config.url && !config.command) {
197
+ return `MCP ${id}: must specify either 'url' or 'command'`;
198
+ }
199
+ if (
200
+ config.url &&
201
+ !config.url.startsWith("http://") &&
202
+ !config.url.startsWith("https://")
203
+ ) {
204
+ return `MCP ${id}: url must be http:// or https://`;
205
+ }
206
+ if (config.command) {
207
+ const dangerousCommands = [
208
+ "rm",
209
+ "sudo",
210
+ "curl",
211
+ "wget",
212
+ "sh",
213
+ "bash",
214
+ "zsh",
215
+ "kill",
216
+ ];
217
+ const baseCommand = config.command.split("/").pop()?.split(" ")[0] || "";
218
+ if (dangerousCommands.includes(baseCommand)) {
219
+ return `MCP ${id}: command '${baseCommand}' is not allowed`;
220
+ }
221
+ }
222
+ return null;
223
+ }
224
+
225
+ function validateMcpConfig(
226
+ mcpServers: Record<string, McpServerConfig>
227
+ ): string | null {
228
+ for (const [id, config] of Object.entries(mcpServers)) {
229
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
230
+ return `MCP ID '${id}' is invalid`;
231
+ }
232
+ const error = validateMcpServerConfig(id, config);
233
+ if (error) return error;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ // =============================================================================
239
+ // Broadcast Functions (exported for use by other modules)
240
+ // =============================================================================
241
+
242
+ export function broadcastToAgent(
243
+ agentId: string,
244
+ event: string,
245
+ data: unknown
246
+ ): void {
247
+ const connections = sseConnections.get(agentId);
248
+ if (!connections || connections.size === 0) return;
249
+
250
+ const deadConnections = new Set<any>();
251
+
252
+ for (const res of connections) {
253
+ try {
254
+ if (res.closed || res.destroyed || res.writableEnded) {
255
+ deadConnections.add(res);
256
+ continue;
257
+ }
258
+ if (typeof res.writeSSE === "function") {
259
+ res.writeSSE({ event, data: JSON.stringify(data) });
260
+ } else if (typeof res.write === "function") {
261
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
262
+ res.write(message);
263
+ }
264
+ } catch {
265
+ deadConnections.add(res);
266
+ }
267
+ }
268
+
269
+ for (const deadRes of deadConnections) {
270
+ connections.delete(deadRes);
271
+ }
272
+ if (connections.size === 0) {
273
+ sseConnections.delete(agentId);
274
+ }
275
+ }
276
+
277
+ // =============================================================================
278
+ // OpenAPI Route Definitions
279
+ // =============================================================================
280
+
281
+ const createAgentRoute = createRoute({
282
+ method: "post",
283
+ path: "/api/v1/agents",
284
+ tags: ["Agents"],
285
+ summary: "Create a new agent",
286
+ security: [{ bearerAuth: [] }],
287
+ description:
288
+ "Creates a new agent session and returns authentication credentials",
289
+ request: {
290
+ body: {
291
+ content: { "application/json": { schema: CreateAgentRequestSchema } },
292
+ },
293
+ },
294
+ responses: {
295
+ 201: {
296
+ description: "Agent created",
297
+ content: { "application/json": { schema: CreateAgentResponseSchema } },
298
+ },
299
+ 400: {
300
+ description: "Invalid request",
301
+ content: { "application/json": { schema: ErrorResponseSchema } },
302
+ },
303
+ 401: {
304
+ description: "Unauthorized",
305
+ content: { "application/json": { schema: ErrorResponseSchema } },
306
+ },
307
+ },
308
+ });
309
+
310
+ const getAgentRoute = createRoute({
311
+ method: "get",
312
+ path: "/api/v1/agents/{agentId}",
313
+ tags: ["Agents"],
314
+ summary: "Get agent status",
315
+ security: [{ bearerAuth: [] }],
316
+ request: { params: AgentIdParamSchema },
317
+ responses: {
318
+ 200: {
319
+ description: "Agent status",
320
+ content: { "application/json": { schema: AgentStatusResponseSchema } },
321
+ },
322
+ 401: {
323
+ description: "Unauthorized",
324
+ content: { "application/json": { schema: ErrorResponseSchema } },
325
+ },
326
+ 404: {
327
+ description: "Not found",
328
+ content: { "application/json": { schema: ErrorResponseSchema } },
329
+ },
330
+ },
331
+ });
332
+
333
+ const deleteAgentRoute = createRoute({
334
+ method: "delete",
335
+ path: "/api/v1/agents/{agentId}",
336
+ tags: ["Agents"],
337
+ summary: "Delete an agent",
338
+ security: [{ bearerAuth: [] }],
339
+ request: { params: AgentIdParamSchema },
340
+ responses: {
341
+ 200: {
342
+ description: "Agent deleted",
343
+ content: { "application/json": { schema: SuccessResponseSchema } },
344
+ },
345
+ 401: {
346
+ description: "Unauthorized",
347
+ content: { "application/json": { schema: ErrorResponseSchema } },
348
+ },
349
+ 404: {
350
+ description: "Not found",
351
+ content: { "application/json": { schema: ErrorResponseSchema } },
352
+ },
353
+ },
354
+ });
355
+
356
+ const getAgentEventsRoute = createRoute({
357
+ method: "get",
358
+ path: "/api/v1/agents/{agentId}/events",
359
+ tags: ["Messages"],
360
+ summary: "Subscribe to agent events (SSE)",
361
+ description: "Server-Sent Events stream for real-time agent updates",
362
+ security: [{ bearerAuth: [] }],
363
+ request: { params: AgentIdParamSchema },
364
+ responses: {
365
+ 200: {
366
+ description: "SSE stream",
367
+ content: { "text/event-stream": { schema: z.string() } },
368
+ },
369
+ 401: {
370
+ description: "Unauthorized",
371
+ content: { "application/json": { schema: ErrorResponseSchema } },
372
+ },
373
+ 429: {
374
+ description: "Too many connections",
375
+ content: { "application/json": { schema: ErrorResponseSchema } },
376
+ },
377
+ },
378
+ });
379
+
380
+ const sendMessageRoute = createRoute({
381
+ method: "post",
382
+ path: "/api/v1/agents/{agentId}/messages",
383
+ tags: ["Messages"],
384
+ summary: "Send a message to the agent",
385
+ description:
386
+ "Send a message to an agent. Supports JSON body or multipart form data for file uploads. " +
387
+ "When platform is specified, the message is routed through the platform adapter.",
388
+ security: [{ bearerAuth: [] }],
389
+ request: {
390
+ params: AgentIdParamSchema,
391
+ body: {
392
+ content: {
393
+ "application/json": { schema: SendMessageRequestSchema },
394
+ },
395
+ },
396
+ },
397
+ responses: {
398
+ 200: {
399
+ description: "Message queued",
400
+ content: { "application/json": { schema: SendMessageResponseSchema } },
401
+ },
402
+ 400: {
403
+ description: "Invalid request",
404
+ content: { "application/json": { schema: ErrorResponseSchema } },
405
+ },
406
+ 401: {
407
+ description: "Unauthorized",
408
+ content: { "application/json": { schema: ErrorResponseSchema } },
409
+ },
410
+ 403: {
411
+ description: "Forbidden - worker tokens cannot route to platforms",
412
+ content: { "application/json": { schema: ErrorResponseSchema } },
413
+ },
414
+ 404: {
415
+ description: "Agent not found",
416
+ content: { "application/json": { schema: ErrorResponseSchema } },
417
+ },
418
+ },
419
+ });
420
+
421
+ // =============================================================================
422
+ // Create OpenAPI Hono App
423
+ // =============================================================================
424
+
425
+ export interface AgentApiConfig {
426
+ queueProducer: QueueProducer;
427
+ sessionManager: ISessionManager;
428
+ publicGatewayUrl: string;
429
+ adminPassword?: string;
430
+ cliTokenService?: CliTokenService;
431
+ externalAuthClient?: ExternalAuthClient;
432
+ agentSettingsStore?: AgentSettingsStore;
433
+ agentConfigStore?: Pick<AgentConfigStore, "getSettings" | "listAgents">;
434
+ platformRegistry?: PlatformRegistry;
435
+ }
436
+
437
+ export function createAgentApi(config: AgentApiConfig): OpenAPIHono;
438
+ export function createAgentApi(
439
+ queueProducer: QueueProducer,
440
+ sessionManager: ISessionManager,
441
+ publicGatewayUrl: string
442
+ ): OpenAPIHono;
443
+ export function createAgentApi(
444
+ configOrQueue: AgentApiConfig | QueueProducer,
445
+ sessionManager?: ISessionManager,
446
+ publicGatewayUrl?: string
447
+ ): OpenAPIHono {
448
+ const config: AgentApiConfig =
449
+ configOrQueue instanceof Object && "queueProducer" in configOrQueue
450
+ ? configOrQueue
451
+ : {
452
+ queueProducer: configOrQueue as QueueProducer,
453
+ sessionManager: sessionManager!,
454
+ publicGatewayUrl: publicGatewayUrl!,
455
+ };
456
+
457
+ const {
458
+ queueProducer,
459
+ adminPassword,
460
+ cliTokenService,
461
+ agentSettingsStore,
462
+ agentConfigStore,
463
+ platformRegistry,
464
+ } = config;
465
+ const sessMgr = config.sessionManager;
466
+ const pubUrl = config.publicGatewayUrl;
467
+ const app = new OpenAPIHono();
468
+
469
+ // Unified auth middleware for all agent API routes
470
+ app.use(
471
+ "/api/v1/agents/*",
472
+ createApiAuthMiddleware({
473
+ adminPassword,
474
+ cliTokenService,
475
+ externalAuthClient: config.externalAuthClient,
476
+ allowSettingsSession: true,
477
+ })
478
+ );
479
+
480
+ // =============================================================================
481
+ // Route Handlers
482
+ // =============================================================================
483
+
484
+ // POST /api/v1/agents - Create agent
485
+ app.openapi(createAgentRoute, async (c): Promise<any> => {
486
+ const body = c.req.valid("json");
487
+ const {
488
+ provider = "claude",
489
+ model,
490
+ agentId: requestedAgentId,
491
+ userId: requestedUserId,
492
+ thread,
493
+ forceNew,
494
+ dryRun,
495
+ networkConfig,
496
+ mcpServers,
497
+ nix: nixConfig,
498
+ } = body;
499
+
500
+ // Validate provider
501
+ if (provider && !["claude"].includes(provider)) {
502
+ return c.json(
503
+ { success: false, error: "Invalid provider. Supported: claude" },
504
+ 400
505
+ );
506
+ }
507
+
508
+ // Validate network config
509
+ if (networkConfig) {
510
+ const error = validateNetworkConfig(networkConfig as NetworkConfig);
511
+ if (error) return c.json({ success: false, error }, 400);
512
+ }
513
+
514
+ // Validate MCP config
515
+ if (mcpServers) {
516
+ const error = validateMcpConfig(
517
+ mcpServers as Record<string, McpServerConfig>
518
+ );
519
+ if (error) return c.json({ success: false, error }, 400);
520
+ }
521
+
522
+ const isEphemeral = !requestedAgentId?.trim();
523
+ const agentId = requestedAgentId?.trim() || randomUUID();
524
+
525
+ // For ephemeral agents, auto-provision settings so the worker gets provider config
526
+ if (isEphemeral && agentSettingsStore) {
527
+ // Try system-key providers first (env var based API keys)
528
+ const providerModules = getModelProviderModules();
529
+ const systemProviders: InstalledProvider[] = providerModules
530
+ .filter((m) => m.hasSystemKey())
531
+ .map((m) => ({
532
+ providerId: m.providerId,
533
+ installedAt: Date.now(),
534
+ }));
535
+
536
+ if (systemProviders.length > 0) {
537
+ // Also inherit pluginsConfig from template agent if available
538
+ const templateId = agentConfigStore
539
+ ? await findTemplateAgentId(agentConfigStore)
540
+ : await agentSettingsStore.findTemplateAgentId();
541
+ const templateSettings = templateId
542
+ ? await (agentConfigStore?.getSettings(templateId) ??
543
+ agentSettingsStore.getSettings(templateId))
544
+ : null;
545
+ await agentSettingsStore.saveSettings(agentId, {
546
+ installedProviders: systemProviders,
547
+ pluginsConfig: templateSettings?.pluginsConfig,
548
+ });
549
+ logger.info(
550
+ `Ephemeral agent ${agentId}: provisioned system providers [${systemProviders.map((p) => p.providerId).join(", ")}]`
551
+ );
552
+ } else {
553
+ // Fall back to using an existing agent as template (inherits its providers)
554
+ const templateId = agentConfigStore
555
+ ? await findTemplateAgentId(agentConfigStore)
556
+ : await agentSettingsStore.findTemplateAgentId();
557
+ if (templateId) {
558
+ const templateSettings = await (agentConfigStore?.getSettings(
559
+ templateId
560
+ ) ?? agentSettingsStore.getSettings(templateId));
561
+ await agentSettingsStore.saveSettings(agentId, {
562
+ templateAgentId: templateId,
563
+ pluginsConfig: templateSettings?.pluginsConfig,
564
+ });
565
+ logger.info(
566
+ `Ephemeral agent ${agentId}: using template ${templateId}`
567
+ );
568
+ }
569
+ }
570
+ }
571
+
572
+ const userId = requestedUserId || agentId;
573
+
574
+ // Build composite conversationId for user-specific sessions
575
+ // Uses _ separator (colons not allowed in BullMQ custom IDs)
576
+ const conversationId = thread
577
+ ? `${agentId}_${userId}_${thread}`
578
+ : `${agentId}_${userId}`;
579
+ const channelId = `api_${userId}`;
580
+ const deploymentName = `api-${agentId.slice(0, 8)}`;
581
+
582
+ // Try to resume existing session (unless forceNew is requested)
583
+ if (!forceNew) {
584
+ const existing = await sessMgr.getSession(conversationId);
585
+ if (existing) {
586
+ // Reuse existing session — touch lastActivity and return existing token
587
+ await sessMgr.touchSession(conversationId);
588
+
589
+ const token = generateWorkerToken(
590
+ agentId,
591
+ conversationId,
592
+ deploymentName,
593
+ {
594
+ channelId,
595
+ agentId,
596
+ platform: "api",
597
+ sessionKey: userId,
598
+ }
599
+ );
600
+
601
+ const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
602
+ const baseUrl = pubUrl || "http://localhost:8080";
603
+
604
+ logger.info(
605
+ `Resumed API session: ${conversationId} (agent=${agentId})`
606
+ );
607
+
608
+ return c.json(
609
+ {
610
+ success: true,
611
+ agentId: conversationId,
612
+ token,
613
+ expiresAt,
614
+ sseUrl: `${baseUrl}/api/v1/agents/${conversationId}/events`,
615
+ messagesUrl: `${baseUrl}/api/v1/agents/${conversationId}/messages`,
616
+ },
617
+ 201
618
+ );
619
+ }
620
+ }
621
+
622
+ const token = generateWorkerToken(agentId, conversationId, deploymentName, {
623
+ channelId,
624
+ agentId,
625
+ platform: "api",
626
+ sessionKey: userId,
627
+ });
628
+
629
+ const expiresAt = Date.now() + TOKEN_EXPIRATION_MS;
630
+
631
+ const session: ThreadSession = {
632
+ conversationId,
633
+ channelId,
634
+ userId,
635
+ threadCreator: userId,
636
+ lastActivity: Date.now(),
637
+ createdAt: Date.now(),
638
+ status: "created",
639
+ provider,
640
+ model,
641
+ networkConfig: networkConfig as NetworkConfig | undefined,
642
+ mcpConfig: mcpServers
643
+ ? { mcpServers: mcpServers as Record<string, McpServerConfig> }
644
+ : undefined,
645
+ nixConfig,
646
+ agentId,
647
+ dryRun: dryRun || false,
648
+ };
649
+ await sessMgr.setSession(session);
650
+
651
+ logger.info(`Created API agent: ${conversationId} (agent=${agentId})`);
652
+
653
+ const baseUrl = pubUrl || "http://localhost:8080";
654
+ return c.json(
655
+ {
656
+ success: true,
657
+ agentId: conversationId,
658
+ token,
659
+ expiresAt,
660
+ sseUrl: `${baseUrl}/api/v1/agents/${conversationId}/events`,
661
+ messagesUrl: `${baseUrl}/api/v1/agents/${conversationId}/messages`,
662
+ },
663
+ 201
664
+ );
665
+ });
666
+
667
+ // GET /api/v1/agents/:agentId - Get status
668
+ app.openapi(getAgentRoute, async (c): Promise<any> => {
669
+ const { agentId: sessionKey } = c.req.valid("param");
670
+
671
+ const session = await sessMgr.getSession(sessionKey);
672
+ if (!session) {
673
+ return c.json({ success: false, error: "Agent not found" }, 404);
674
+ }
675
+
676
+ const hasActiveConnection =
677
+ sseConnections.has(sessionKey) &&
678
+ (sseConnections.get(sessionKey)?.size ?? 0) > 0;
679
+
680
+ return c.json({
681
+ success: true,
682
+ agent: {
683
+ agentId: session.conversationId,
684
+ userId: session.userId,
685
+ status: session.status || "active",
686
+ createdAt: session.createdAt,
687
+ lastActivity: session.lastActivity,
688
+ hasActiveConnection,
689
+ },
690
+ });
691
+ });
692
+
693
+ // DELETE /api/v1/agents/:agentId
694
+ app.openapi(deleteAgentRoute, async (c): Promise<any> => {
695
+ const { agentId: sessionKey } = c.req.valid("param");
696
+
697
+ const connections = sseConnections.get(sessionKey);
698
+ if (connections) {
699
+ for (const connection of connections) {
700
+ try {
701
+ if (typeof connection.writeSSE === "function") {
702
+ connection.writeSSE({
703
+ event: "closed",
704
+ data: JSON.stringify({ reason: "agent_deleted" }),
705
+ });
706
+ } else if (typeof connection.write === "function") {
707
+ connection.write(
708
+ `event: closed\ndata: ${JSON.stringify({ reason: "agent_deleted" })}\n\n`
709
+ );
710
+ }
711
+ connection.close?.();
712
+ connection.end?.();
713
+ } catch {
714
+ // Ignore
715
+ }
716
+ }
717
+ sseConnections.delete(sessionKey);
718
+ }
719
+
720
+ // Get real agentId from session before deleting
721
+ const session = await sessMgr.getSession(sessionKey);
722
+ const realAgentId = session?.agentId || sessionKey;
723
+
724
+ await sessMgr.deleteSession(sessionKey);
725
+ // Clean up ephemeral agent settings
726
+ if (agentSettingsStore) {
727
+ await agentSettingsStore.deleteSettings(realAgentId).catch(() => {
728
+ /* best-effort cleanup */
729
+ });
730
+ }
731
+ logger.info(`Deleted agent ${sessionKey}`);
732
+
733
+ return c.json({
734
+ success: true,
735
+ message: "Agent deleted",
736
+ agentId: sessionKey,
737
+ });
738
+ });
739
+
740
+ // GET /api/v1/agents/:agentId/events - SSE stream
741
+ app.openapi(getAgentEventsRoute, async (c): Promise<any> => {
742
+ const { agentId: sessionKey } = c.req.valid("param");
743
+
744
+ const session = await sessMgr.getSession(sessionKey);
745
+ if (!session) {
746
+ return c.json({ success: false, error: "Agent not found" }, 404);
747
+ }
748
+
749
+ // Check connection limits
750
+ const totalConnections = Array.from(sseConnections.values()).reduce(
751
+ (acc, set) => acc + set.size,
752
+ 0
753
+ );
754
+ if (totalConnections >= MAX_TOTAL_CONNECTIONS) {
755
+ return c.json(
756
+ { success: false, error: "Server connection limit reached" },
757
+ 429
758
+ );
759
+ }
760
+
761
+ // Use conversationId as the SSE connection key (matches broadcastToAgent calls)
762
+ const sseKey = session.conversationId;
763
+ if (!sseConnections.has(sseKey)) {
764
+ sseConnections.set(sseKey, new Set());
765
+ }
766
+ const agentConnections = sseConnections.get(sseKey)!;
767
+ if (agentConnections.size >= MAX_CONNECTIONS_PER_AGENT) {
768
+ return c.json(
769
+ {
770
+ success: false,
771
+ error: `Maximum ${MAX_CONNECTIONS_PER_AGENT} connections`,
772
+ },
773
+ 429
774
+ );
775
+ }
776
+
777
+ // Return SSE stream
778
+ return streamSSE(c, async (stream) => {
779
+ agentConnections.add(stream);
780
+
781
+ await stream.writeSSE({
782
+ event: "connected",
783
+ data: JSON.stringify({
784
+ agentId: session.agentId || sessionKey,
785
+ timestamp: Date.now(),
786
+ }),
787
+ });
788
+
789
+ const heartbeatInterval = setInterval(async () => {
790
+ try {
791
+ await stream.writeSSE({
792
+ event: "ping",
793
+ data: JSON.stringify({ timestamp: Date.now() }),
794
+ });
795
+ } catch {
796
+ clearInterval(heartbeatInterval);
797
+ }
798
+ }, 30000);
799
+
800
+ stream.onAbort(() => {
801
+ clearInterval(heartbeatInterval);
802
+ agentConnections.delete(stream);
803
+ if (agentConnections.size === 0) {
804
+ sseConnections.delete(sseKey);
805
+ }
806
+ logger.info(`SSE connection closed for session ${sseKey}`);
807
+ });
808
+
809
+ while (true) {
810
+ await stream.sleep(1000);
811
+ }
812
+ });
813
+ });
814
+
815
+ // POST /api/v1/agents/:agentId/messages - Send message
816
+ // Supports two paths:
817
+ // 1. Direct API (no platform field): requires pre-created session, enqueues directly
818
+ // 2. Platform-routed (platform field present): delegates to platform adapter
819
+ app.openapi(sendMessageRoute, async (c): Promise<any> => {
820
+ const { agentId } = c.req.valid("param");
821
+
822
+ // Parse body — multipart for file uploads, JSON otherwise
823
+ const contentType = c.req.header("content-type") || "";
824
+ let body: Record<string, any>;
825
+ let files: Array<{ buffer: Buffer; filename: string }> | undefined;
826
+
827
+ if (contentType.includes("multipart/form-data")) {
828
+ const formData = await c.req.formData();
829
+ body = {
830
+ content: formData.get("content") as string | null,
831
+ message: formData.get("message") as string | null,
832
+ messageId: formData.get("messageId") as string | null,
833
+ platform: formData.get("platform") as string | null,
834
+ };
835
+
836
+ // Extract nested platform routing from form fields
837
+ const slackChannel = formData.get("slack.channel") as string;
838
+ if (slackChannel) {
839
+ body.slack = {
840
+ channel: slackChannel,
841
+ thread: formData.get("slack.thread") as string | undefined,
842
+ team: formData.get("slack.team") as string | undefined,
843
+ };
844
+ }
845
+ const whatsappChat = formData.get("whatsapp.chat") as string;
846
+ if (whatsappChat) {
847
+ body.whatsapp = { chat: whatsappChat };
848
+ }
849
+ const telegramChatId = formData.get("telegram.chatId") as string;
850
+ if (telegramChatId) {
851
+ body.telegram = { chatId: telegramChatId };
852
+ }
853
+
854
+ // Extract files with size validation
855
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
856
+ const MAX_TOTAL_SIZE = 100 * 1024 * 1024;
857
+ const MAX_FILE_COUNT = 10;
858
+ const fileEntries = formData.getAll("files");
859
+ if (fileEntries.length > MAX_FILE_COUNT) {
860
+ return c.json(
861
+ {
862
+ success: false,
863
+ error: `Too many files: ${fileEntries.length} (max ${MAX_FILE_COUNT})`,
864
+ },
865
+ 400
866
+ );
867
+ }
868
+ if (fileEntries.length > 0) {
869
+ const fileResults: Array<{ buffer: Buffer; filename: string }> = [];
870
+ let totalSize = 0;
871
+ for (const entry of fileEntries) {
872
+ if (entry instanceof File) {
873
+ if (entry.size > MAX_FILE_SIZE) {
874
+ return c.json(
875
+ {
876
+ success: false,
877
+ error: `File "${entry.name}" exceeds maximum size of ${MAX_FILE_SIZE / 1024 / 1024}MB`,
878
+ },
879
+ 400
880
+ );
881
+ }
882
+ totalSize += entry.size;
883
+ if (totalSize > MAX_TOTAL_SIZE) {
884
+ return c.json(
885
+ {
886
+ success: false,
887
+ error: `Total upload size exceeds maximum of ${MAX_TOTAL_SIZE / 1024 / 1024}MB`,
888
+ },
889
+ 400
890
+ );
891
+ }
892
+ const arrayBuffer = await entry.arrayBuffer();
893
+ fileResults.push({
894
+ buffer: Buffer.from(arrayBuffer),
895
+ filename: entry.name,
896
+ });
897
+ }
898
+ }
899
+ if (fileResults.length > 0) files = fileResults;
900
+ }
901
+ } else {
902
+ body = c.req.valid("json");
903
+ }
904
+
905
+ const messageContent = body.content || body.message;
906
+ const messageId = body.messageId || randomUUID();
907
+
908
+ if (!messageContent || typeof messageContent !== "string") {
909
+ return c.json({ success: false, error: "content is required" }, 400);
910
+ }
911
+
912
+ const platform = body.platform as string | undefined;
913
+
914
+ // ── Platform-routed path ──────────────────────────────────────────────────
915
+ // When platform is specified, delegate to the platform adapter which handles
916
+ // session creation, routing, and file delivery.
917
+ if (platform) {
918
+ // Worker tokens cannot route to user-facing platform connections
919
+ const authHeader = c.req.header("Authorization");
920
+ const rawToken = authHeader?.startsWith("Bearer ")
921
+ ? authHeader.substring(7)
922
+ : "";
923
+ if (verifyWorkerToken(rawToken)) {
924
+ return c.json(
925
+ { success: false, error: "Worker tokens cannot route to platforms" },
926
+ 403
927
+ );
928
+ }
929
+
930
+ if (!platformRegistry) {
931
+ return c.json(
932
+ { success: false, error: "Platform routing not available" },
933
+ 501
934
+ );
935
+ }
936
+
937
+ const adapter = platformRegistry.get(platform);
938
+ if (!adapter) {
939
+ return c.json(
940
+ {
941
+ success: false,
942
+ error: `Platform "${platform}" not found`,
943
+ details: `Available: ${platformRegistry.getAvailablePlatforms().join(", ")}`,
944
+ },
945
+ 404
946
+ );
947
+ }
948
+
949
+ if (!adapter.sendMessage) {
950
+ return c.json(
951
+ {
952
+ success: false,
953
+ error: `Platform "${platform}" does not support sendMessage`,
954
+ },
955
+ 501
956
+ );
957
+ }
958
+
959
+ // Extract platform-specific routing info
960
+ let channelId = agentId;
961
+ let conversationId: string | undefined =
962
+ platform === "api" ? agentId : undefined;
963
+ let teamId = "api";
964
+
965
+ if (adapter.extractRoutingInfo) {
966
+ const routingInfo = adapter.extractRoutingInfo(
967
+ body as Record<string, unknown>
968
+ );
969
+ if (routingInfo) {
970
+ channelId = routingInfo.channelId;
971
+ conversationId = routingInfo.conversationId || conversationId;
972
+ teamId = routingInfo.teamId || "api";
973
+ } else if (platform !== "api") {
974
+ return c.json(
975
+ {
976
+ success: false,
977
+ error: `Platform-specific routing info required for ${platform}`,
978
+ },
979
+ 400
980
+ );
981
+ }
982
+ }
983
+
984
+ logger.info(
985
+ `Sending message via ${platform}: agentId=${agentId}, channelId=${channelId}${files?.length ? `, files=${files.length}` : ""}`
986
+ );
987
+
988
+ try {
989
+ const result = await adapter.sendMessage(rawToken, messageContent, {
990
+ agentId,
991
+ channelId,
992
+ conversationId,
993
+ teamId,
994
+ files,
995
+ });
996
+
997
+ return c.json({
998
+ success: true,
999
+ agentId,
1000
+ messageId: result.messageId,
1001
+ eventsUrl: result.eventsUrl,
1002
+ queued: result.queued || false,
1003
+ });
1004
+ } catch (error) {
1005
+ logger.error("Failed to send platform message", { error });
1006
+ return c.json({ success: false, error: "Internal server error" }, 500);
1007
+ }
1008
+ }
1009
+
1010
+ // ── Direct API path ───────────────────────────────────────────────────────
1011
+ // No platform field: use existing session-based direct enqueue
1012
+ const session = await sessMgr.getSession(agentId);
1013
+ if (!session) {
1014
+ return c.json({ success: false, error: "Agent not found" }, 404);
1015
+ }
1016
+
1017
+ await sessMgr.touchSession(agentId);
1018
+
1019
+ const realAgentId = session.agentId || agentId;
1020
+
1021
+ const { span: rootSpan, traceparent } = createRootSpan("message_received", {
1022
+ "lobu.agent_id": realAgentId,
1023
+ "lobu.message_id": messageId,
1024
+ });
1025
+
1026
+ try {
1027
+ const channelId = session.channelId || `api_${session.userId}`;
1028
+
1029
+ const baseOptions: Record<string, any> = {
1030
+ provider: session.provider || "claude",
1031
+ model: session.model,
1032
+ };
1033
+ const agentOptions = await resolveAgentOptions(
1034
+ realAgentId,
1035
+ baseOptions,
1036
+ agentSettingsStore
1037
+ );
1038
+
1039
+ const {
1040
+ networkConfig: settingsNetwork,
1041
+ mcpServers: settingsMcpServers,
1042
+ ...remainingOptions
1043
+ } = agentOptions;
1044
+
1045
+ const jobId = await queueProducer.enqueueMessage({
1046
+ userId: session.userId,
1047
+ conversationId: session.conversationId || agentId,
1048
+ messageId,
1049
+ channelId,
1050
+ teamId: "api",
1051
+ agentId: realAgentId,
1052
+ botId: "lobu-api",
1053
+ platform: "api",
1054
+ messageText: messageContent,
1055
+ platformMetadata: {
1056
+ agentId: realAgentId,
1057
+ source: "direct-api",
1058
+ traceparent: traceparent || undefined,
1059
+ dryRun: session.dryRun || false,
1060
+ },
1061
+ agentOptions: remainingOptions,
1062
+ networkConfig: session.networkConfig || settingsNetwork,
1063
+ mcpConfig:
1064
+ session.mcpConfig ||
1065
+ (settingsMcpServers ? { mcpServers: settingsMcpServers } : undefined),
1066
+ });
1067
+
1068
+ rootSpan?.end();
1069
+
1070
+ return c.json({
1071
+ success: true,
1072
+ messageId,
1073
+ jobId,
1074
+ queued: true,
1075
+ traceparent: traceparent || undefined,
1076
+ });
1077
+ } catch (error) {
1078
+ rootSpan?.end();
1079
+ throw error;
1080
+ }
1081
+ });
1082
+
1083
+ logger.debug("Hono Agent API routes registered");
1084
+
1085
+ return app;
1086
+ }