@lobu/gateway 3.0.9 → 3.0.13

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 (212) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +7 -26
  3. package/dist/api/platform.js.map +1 -1
  4. package/dist/auth/mcp/proxy.d.ts +14 -0
  5. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  6. package/dist/auth/mcp/proxy.js +149 -13
  7. package/dist/auth/mcp/proxy.js.map +1 -1
  8. package/dist/cli/gateway.d.ts.map +1 -1
  9. package/dist/cli/gateway.js +29 -0
  10. package/dist/cli/gateway.js.map +1 -1
  11. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  12. package/dist/connections/chat-instance-manager.js +2 -1
  13. package/dist/connections/chat-instance-manager.js.map +1 -1
  14. package/dist/connections/interaction-bridge.d.ts +9 -2
  15. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  16. package/dist/connections/interaction-bridge.js +121 -261
  17. package/dist/connections/interaction-bridge.js.map +1 -1
  18. package/dist/gateway/index.js +1 -1
  19. package/dist/gateway/index.js.map +1 -1
  20. package/dist/interactions.d.ts +9 -43
  21. package/dist/interactions.d.ts.map +1 -1
  22. package/dist/interactions.js +10 -52
  23. package/dist/interactions.js.map +1 -1
  24. package/dist/routes/public/agent.d.ts +4 -0
  25. package/dist/routes/public/agent.d.ts.map +1 -1
  26. package/dist/routes/public/agent.js +21 -0
  27. package/dist/routes/public/agent.js.map +1 -1
  28. package/dist/services/core-services.d.ts.map +1 -1
  29. package/dist/services/core-services.js +4 -0
  30. package/dist/services/core-services.js.map +1 -1
  31. package/package.json +9 -9
  32. package/src/__tests__/agent-config-routes.test.ts +0 -254
  33. package/src/__tests__/agent-history-routes.test.ts +0 -72
  34. package/src/__tests__/agent-routes.test.ts +0 -68
  35. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  36. package/src/__tests__/agent-settings-store.test.ts +0 -323
  37. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  38. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  39. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  40. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  41. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  42. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  43. package/src/__tests__/config-request-store.test.ts +0 -127
  44. package/src/__tests__/connection-routes.test.ts +0 -144
  45. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  46. package/src/__tests__/docker-deployment.test.ts +0 -1211
  47. package/src/__tests__/embedded-deployment.test.ts +0 -342
  48. package/src/__tests__/grant-store.test.ts +0 -148
  49. package/src/__tests__/http-proxy.test.ts +0 -281
  50. package/src/__tests__/instruction-service.test.ts +0 -37
  51. package/src/__tests__/link-buttons.test.ts +0 -112
  52. package/src/__tests__/lobu.test.ts +0 -32
  53. package/src/__tests__/mcp-config-service.test.ts +0 -347
  54. package/src/__tests__/mcp-proxy.test.ts +0 -694
  55. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  56. package/src/__tests__/model-selection.test.ts +0 -172
  57. package/src/__tests__/oauth-templates.test.ts +0 -39
  58. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  59. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  60. package/src/__tests__/provider-inheritance.test.ts +0 -212
  61. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  62. package/src/__tests__/routes/interactions.test.ts +0 -121
  63. package/src/__tests__/secret-proxy.test.ts +0 -85
  64. package/src/__tests__/session-manager.test.ts +0 -572
  65. package/src/__tests__/setup.ts +0 -133
  66. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  67. package/src/__tests__/slack-routes.test.ts +0 -161
  68. package/src/__tests__/system-config-resolver.test.ts +0 -75
  69. package/src/__tests__/system-message-limiter.test.ts +0 -89
  70. package/src/__tests__/system-skills-service.test.ts +0 -362
  71. package/src/__tests__/transcription-service.test.ts +0 -222
  72. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  73. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  74. package/src/__tests__/worker-job-router.test.ts +0 -722
  75. package/src/api/index.ts +0 -1
  76. package/src/api/platform.ts +0 -292
  77. package/src/api/response-renderer.ts +0 -157
  78. package/src/auth/agent-metadata-store.ts +0 -168
  79. package/src/auth/api-auth-middleware.ts +0 -69
  80. package/src/auth/api-key-provider-module.ts +0 -213
  81. package/src/auth/base-provider-module.ts +0 -201
  82. package/src/auth/bedrock/provider-module.ts +0 -110
  83. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  84. package/src/auth/chatgpt/device-code-client.ts +0 -218
  85. package/src/auth/chatgpt/index.ts +0 -1
  86. package/src/auth/claude/oauth-module.ts +0 -280
  87. package/src/auth/cli/token-service.ts +0 -249
  88. package/src/auth/external/client.ts +0 -560
  89. package/src/auth/external/device-code-client.ts +0 -235
  90. package/src/auth/mcp/config-service.ts +0 -420
  91. package/src/auth/mcp/proxy.ts +0 -1086
  92. package/src/auth/mcp/string-substitution.ts +0 -17
  93. package/src/auth/mcp/tool-cache.ts +0 -90
  94. package/src/auth/oauth/base-client.ts +0 -267
  95. package/src/auth/oauth/client.ts +0 -153
  96. package/src/auth/oauth/credentials.ts +0 -7
  97. package/src/auth/oauth/providers.ts +0 -69
  98. package/src/auth/oauth/state-store.ts +0 -150
  99. package/src/auth/oauth-templates.ts +0 -179
  100. package/src/auth/provider-catalog.ts +0 -220
  101. package/src/auth/provider-model-options.ts +0 -41
  102. package/src/auth/settings/agent-settings-store.ts +0 -565
  103. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  104. package/src/auth/settings/index.ts +0 -12
  105. package/src/auth/settings/model-preference-store.ts +0 -52
  106. package/src/auth/settings/model-selection.ts +0 -135
  107. package/src/auth/settings/resolved-settings-view.ts +0 -298
  108. package/src/auth/settings/template-utils.ts +0 -44
  109. package/src/auth/settings/token-service.ts +0 -88
  110. package/src/auth/system-env-store.ts +0 -98
  111. package/src/auth/user-agents-store.ts +0 -68
  112. package/src/channels/binding-service.ts +0 -214
  113. package/src/channels/index.ts +0 -4
  114. package/src/cli/gateway.ts +0 -1312
  115. package/src/cli/index.ts +0 -74
  116. package/src/commands/built-in-commands.ts +0 -80
  117. package/src/commands/command-dispatcher.ts +0 -94
  118. package/src/commands/command-reply-adapters.ts +0 -27
  119. package/src/config/file-loader.ts +0 -618
  120. package/src/config/index.ts +0 -588
  121. package/src/config/network-allowlist.ts +0 -71
  122. package/src/connections/chat-instance-manager.ts +0 -1284
  123. package/src/connections/chat-response-bridge.ts +0 -618
  124. package/src/connections/index.ts +0 -7
  125. package/src/connections/interaction-bridge.ts +0 -831
  126. package/src/connections/message-handler-bridge.ts +0 -440
  127. package/src/connections/platform-auth-methods.ts +0 -15
  128. package/src/connections/types.ts +0 -84
  129. package/src/gateway/connection-manager.ts +0 -291
  130. package/src/gateway/index.ts +0 -698
  131. package/src/gateway/job-router.ts +0 -201
  132. package/src/gateway-main.ts +0 -200
  133. package/src/index.ts +0 -41
  134. package/src/infrastructure/queue/index.ts +0 -12
  135. package/src/infrastructure/queue/queue-producer.ts +0 -148
  136. package/src/infrastructure/queue/redis-queue.ts +0 -361
  137. package/src/infrastructure/queue/types.ts +0 -133
  138. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  139. package/src/interactions/config-request-store.ts +0 -198
  140. package/src/interactions.ts +0 -363
  141. package/src/lobu.ts +0 -311
  142. package/src/metrics/prometheus.ts +0 -159
  143. package/src/modules/module-system.ts +0 -179
  144. package/src/orchestration/base-deployment-manager.ts +0 -900
  145. package/src/orchestration/deployment-utils.ts +0 -98
  146. package/src/orchestration/impl/docker-deployment.ts +0 -620
  147. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  148. package/src/orchestration/impl/index.ts +0 -8
  149. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  150. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  151. package/src/orchestration/impl/k8s/index.ts +0 -1
  152. package/src/orchestration/index.ts +0 -333
  153. package/src/orchestration/message-consumer.ts +0 -584
  154. package/src/orchestration/scheduled-wakeup.ts +0 -704
  155. package/src/permissions/approval-policy.ts +0 -36
  156. package/src/permissions/grant-store.ts +0 -219
  157. package/src/platform/file-handler.ts +0 -66
  158. package/src/platform/link-buttons.ts +0 -57
  159. package/src/platform/renderer-utils.ts +0 -44
  160. package/src/platform/response-renderer.ts +0 -84
  161. package/src/platform/unified-thread-consumer.ts +0 -194
  162. package/src/platform.ts +0 -318
  163. package/src/proxy/http-proxy.ts +0 -752
  164. package/src/proxy/proxy-manager.ts +0 -81
  165. package/src/proxy/secret-proxy.ts +0 -402
  166. package/src/proxy/token-refresh-job.ts +0 -143
  167. package/src/routes/internal/audio.ts +0 -141
  168. package/src/routes/internal/device-auth.ts +0 -652
  169. package/src/routes/internal/files.ts +0 -226
  170. package/src/routes/internal/history.ts +0 -69
  171. package/src/routes/internal/images.ts +0 -127
  172. package/src/routes/internal/interactions.ts +0 -84
  173. package/src/routes/internal/middleware.ts +0 -23
  174. package/src/routes/internal/schedule.ts +0 -226
  175. package/src/routes/internal/types.ts +0 -22
  176. package/src/routes/openapi-auto.ts +0 -239
  177. package/src/routes/public/agent-access.ts +0 -23
  178. package/src/routes/public/agent-config.ts +0 -675
  179. package/src/routes/public/agent-history.ts +0 -422
  180. package/src/routes/public/agent-schedules.ts +0 -296
  181. package/src/routes/public/agent.ts +0 -1086
  182. package/src/routes/public/agents.ts +0 -373
  183. package/src/routes/public/channels.ts +0 -191
  184. package/src/routes/public/cli-auth.ts +0 -896
  185. package/src/routes/public/connections.ts +0 -574
  186. package/src/routes/public/landing.ts +0 -16
  187. package/src/routes/public/oauth.ts +0 -147
  188. package/src/routes/public/settings-auth.ts +0 -104
  189. package/src/routes/public/slack.ts +0 -173
  190. package/src/routes/shared/agent-ownership.ts +0 -101
  191. package/src/routes/shared/token-verifier.ts +0 -34
  192. package/src/services/bedrock-model-catalog.ts +0 -217
  193. package/src/services/bedrock-openai-service.ts +0 -658
  194. package/src/services/core-services.ts +0 -1072
  195. package/src/services/image-generation-service.ts +0 -257
  196. package/src/services/instruction-service.ts +0 -318
  197. package/src/services/mcp-registry.ts +0 -94
  198. package/src/services/platform-helpers.ts +0 -287
  199. package/src/services/session-manager.ts +0 -262
  200. package/src/services/settings-resolver.ts +0 -74
  201. package/src/services/system-config-resolver.ts +0 -89
  202. package/src/services/system-skills-service.ts +0 -229
  203. package/src/services/transcription-service.ts +0 -684
  204. package/src/session.ts +0 -110
  205. package/src/spaces/index.ts +0 -1
  206. package/src/spaces/space-resolver.ts +0 -17
  207. package/src/stores/in-memory-agent-store.ts +0 -403
  208. package/src/stores/redis-agent-store.ts +0 -279
  209. package/src/utils/public-url.ts +0 -44
  210. package/src/utils/rate-limiter.ts +0 -94
  211. package/tsconfig.json +0 -33
  212. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1312 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import type { Server } from "node:http";
4
- import { createServer } from "node:http";
5
- import { getRequestListener } from "@hono/node-server";
6
- import { OpenAPIHono } from "@hono/zod-openapi";
7
- import { createLogger } from "@lobu/core";
8
- import { apiReference } from "@scalar/hono-api-reference";
9
- import { cors } from "hono/cors";
10
- import { secureHeaders } from "hono/secure-headers";
11
- import type { AgentMetadata } from "../auth/agent-metadata-store";
12
- import type { GatewayConfig } from "../config";
13
- import { getModelProviderModules } from "../modules/module-system";
14
- import { registerAutoOpenApiRoutes } from "../routes/openapi-auto";
15
-
16
- const logger = createLogger("gateway-startup");
17
-
18
- let httpServer: Server | null = null;
19
-
20
- export interface CreateGatewayAppOptions {
21
- secretProxy: any;
22
- workerGateway: any;
23
- mcpProxy: any;
24
- interactionService?: any;
25
- platformRegistry?: any;
26
- coreServices?: any;
27
- chatInstanceManager?: import("../connections").ChatInstanceManager | null;
28
- /** Custom auth provider for embedded mode. When set, gateway delegates auth to this function instead of using cookie-based sessions. */
29
- authProvider?: import("../routes/public/settings-auth").AuthProvider;
30
- }
31
-
32
- /**
33
- * Create the Hono app with all gateway routes.
34
- * Returns the app without starting an HTTP server — the caller can mount it
35
- * on their own server (embedded mode) or pass it to `startGatewayServer()`.
36
- */
37
- export function createGatewayApp(
38
- options: CreateGatewayAppOptions
39
- ): OpenAPIHono {
40
- const {
41
- secretProxy,
42
- workerGateway,
43
- mcpProxy,
44
- interactionService,
45
- platformRegistry,
46
- coreServices,
47
- chatInstanceManager,
48
- authProvider,
49
- } = options;
50
-
51
- // Wire injectable auth provider (for embedded mode)
52
- if (authProvider) {
53
- const { setAuthProvider } = require("../routes/public/settings-auth");
54
- setAuthProvider(authProvider);
55
- }
56
-
57
- const app = new OpenAPIHono();
58
-
59
- // Global middleware
60
- app.use(
61
- "*",
62
- secureHeaders({
63
- xFrameOptions: false,
64
- xContentTypeOptions: "nosniff",
65
- referrerPolicy: "strict-origin-when-cross-origin",
66
- strictTransportSecurity: "max-age=63072000; includeSubDomains",
67
- contentSecurityPolicy: {
68
- defaultSrc: ["'self'"],
69
- frameAncestors: ["'self'", "*"],
70
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
71
- styleSrc: ["'self'", "'unsafe-inline'"],
72
- imgSrc: ["'self'", "data:", "https:"],
73
- connectSrc: ["'self'", "ws:", "wss:"],
74
- fontSrc: ["'self'", "https://fonts.gstatic.com"],
75
- },
76
- })
77
- );
78
- app.use(
79
- "*",
80
- cors({
81
- origin: process.env.ALLOWED_ORIGINS
82
- ? process.env.ALLOWED_ORIGINS.split(",")
83
- : [],
84
- credentials: true,
85
- })
86
- );
87
-
88
- // Health endpoints
89
- app.get("/health", (c) => {
90
- const mode =
91
- process.env.LOBU_MODE ||
92
- (process.env.DEPLOYMENT_MODE === "docker" ? "local" : "cloud");
93
-
94
- return c.json({
95
- status: "ok",
96
- mode,
97
- version: process.env.npm_package_version || "2.3.0",
98
- timestamp: new Date().toISOString(),
99
- publicGatewayUrl:
100
- coreServices?.getPublicGatewayUrl?.() || process.env.PUBLIC_GATEWAY_URL,
101
- capabilities: {
102
- agents: ["claude"],
103
- streaming: true,
104
- toolApproval: true,
105
- },
106
- wsUrl: `ws://localhost:8080/ws`,
107
- secretProxy: !!secretProxy,
108
- });
109
- });
110
-
111
- app.get("/ready", (c) => c.json({ ready: true }));
112
-
113
- // Compute adminPassword once — used by Agent API, CLI auth, metrics, and messaging
114
- const crypto = require("node:crypto");
115
- const adminPassword: string =
116
- process.env.ADMIN_PASSWORD || crypto.randomBytes(16).toString("base64url");
117
-
118
- // Prometheus metrics endpoint.
119
- // Keep auth optional so existing ServiceMonitor configs continue to scrape.
120
- app.get("/metrics", async (c) => {
121
- const metricsAuthToken = process.env.METRICS_AUTH_TOKEN;
122
- if (metricsAuthToken) {
123
- const authHeader = c.req.header("Authorization");
124
- if (authHeader !== `Bearer ${metricsAuthToken}`) {
125
- return c.text("Unauthorized", 401);
126
- }
127
- }
128
- const { getMetricsText } = await import("../metrics/prometheus");
129
- c.header("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
130
- return c.text(getMetricsText());
131
- });
132
-
133
- // Secret injection proxy (Hono)
134
- if (secretProxy) {
135
- app.route("/api/proxy", secretProxy.getApp());
136
- logger.debug("Secret proxy enabled at :8080/api/proxy");
137
- }
138
-
139
- if (coreServices) {
140
- const bedrockOpenAIService = coreServices.getBedrockOpenAIService?.();
141
- if (bedrockOpenAIService) {
142
- app.route("/api/bedrock", bedrockOpenAIService.getApp());
143
- logger.debug("Bedrock routes enabled at :8080/api/bedrock/*");
144
- }
145
- }
146
-
147
- // Worker Gateway routes (Hono)
148
- if (workerGateway) {
149
- app.route("/worker", workerGateway.getApp());
150
- logger.debug("Worker gateway routes enabled at :8080/worker/*");
151
- }
152
-
153
- // Register module endpoints
154
- const { moduleRegistry: coreModuleRegistry } = require("@lobu/core");
155
- if (coreModuleRegistry.registerHonoEndpoints) {
156
- coreModuleRegistry.registerHonoEndpoints(app);
157
- } else {
158
- // Create express-like adapter for module registry
159
- const expressApp = createExpressAdapter(app);
160
- coreModuleRegistry.registerEndpoints(expressApp);
161
- }
162
- logger.debug("Module endpoints registered");
163
-
164
- // MCP proxy routes (Hono)
165
- if (mcpProxy) {
166
- // Handle root path requests with X-Mcp-Id header
167
- app.all("/", async (c, next) => {
168
- if (mcpProxy.isMcpRequest(c)) {
169
- // Forward to MCP proxy - need to handle directly since it's at root
170
- return mcpProxy.getApp().fetch(c.req.raw);
171
- }
172
- return next();
173
- });
174
- // Mount MCP proxy at /mcp/*
175
- app.route("/mcp", mcpProxy.getApp());
176
- logger.debug("MCP proxy routes enabled at :8080/mcp/*");
177
- }
178
-
179
- // File routes (already Hono) - uses platform registry for per-platform file handling
180
- if (platformRegistry) {
181
- const { createFileRoutes } = require("../routes/internal/files");
182
- const fileRouter = createFileRoutes(platformRegistry);
183
- app.route("/internal/files", fileRouter);
184
- logger.debug("File routes enabled at :8080/internal/files/*");
185
- }
186
-
187
- // History routes (already Hono)
188
- {
189
- const { createHistoryRoutes } = require("../routes/internal/history");
190
- const historyRouter = createHistoryRoutes();
191
- app.route("/internal", historyRouter);
192
- logger.debug("History routes enabled at :8080/internal/history");
193
- }
194
-
195
- // Schedule routes (worker scheduling endpoints)
196
- if (coreServices) {
197
- const scheduledWakeupService = coreServices.getScheduledWakeupService();
198
- if (scheduledWakeupService) {
199
- const { createScheduleRoutes } = require("../routes/internal/schedule");
200
- const scheduleRouter = createScheduleRoutes(scheduledWakeupService);
201
- app.route("", scheduleRouter);
202
- logger.debug("Schedule routes enabled at :8080/internal/schedule");
203
- }
204
- }
205
-
206
- // Device auth routes (gateway-mediated OAuth for workers)
207
- if (coreServices) {
208
- const {
209
- createDeviceAuthRoutes,
210
- } = require("../routes/internal/device-auth");
211
- const redisClient = coreServices.getQueue().getRedisClient();
212
- const mcpConfigService = coreServices.getMcpConfigService();
213
- if (mcpConfigService) {
214
- const deviceAuthRouter = createDeviceAuthRoutes({
215
- redis: redisClient,
216
- mcpConfigService,
217
- });
218
- app.route("", deviceAuthRouter);
219
- logger.debug(
220
- "Device auth routes enabled at :8080/internal/device-auth/*"
221
- );
222
- }
223
- }
224
-
225
- // Audio routes (TTS synthesis for workers)
226
- if (coreServices) {
227
- const transcriptionService = coreServices.getTranscriptionService();
228
- if (transcriptionService) {
229
- const { createAudioRoutes } = require("../routes/internal/audio");
230
- const audioRouter = createAudioRoutes(transcriptionService);
231
- app.route("", audioRouter);
232
- logger.debug("Audio routes enabled at :8080/internal/audio/*");
233
- }
234
- }
235
-
236
- // Image routes (image generation for workers)
237
- if (coreServices) {
238
- const imageGenerationService = coreServices.getImageGenerationService();
239
- if (imageGenerationService) {
240
- const { createImageRoutes } = require("../routes/internal/images");
241
- const imageRouter = createImageRoutes(imageGenerationService);
242
- app.route("", imageRouter);
243
- logger.debug("Image routes enabled at :8080/internal/images/*");
244
- }
245
- }
246
-
247
- // Interaction routes (already Hono)
248
- if (interactionService) {
249
- const {
250
- createInteractionRoutes,
251
- } = require("../routes/internal/interactions");
252
- const internalRouter = createInteractionRoutes(interactionService);
253
- app.route("", internalRouter);
254
- logger.debug("Internal interaction routes enabled");
255
- }
256
-
257
- // Create CLI token service early so it can be shared by messaging + agent API
258
- let cliTokenService: any;
259
- if (coreServices) {
260
- const { CliTokenService } = require("../auth/cli/token-service");
261
- const redisClient = coreServices.getQueue().getRedisClient();
262
- cliTokenService = new CliTokenService(redisClient);
263
- }
264
-
265
- // Agent API routes (direct API access + platform-routed messaging)
266
- if (coreServices) {
267
- const queueProducer = coreServices.getQueueProducer();
268
- const sessionMgr = coreServices.getSessionManager();
269
- const interactionSvc = coreServices.getInteractionService();
270
- const publicUrl = coreServices.getPublicGatewayUrl();
271
-
272
- if (queueProducer && sessionMgr && interactionSvc) {
273
- const { createAgentApi } = require("../routes/public/agent");
274
- const agentApi = createAgentApi({
275
- queueProducer,
276
- sessionManager: sessionMgr,
277
- publicGatewayUrl: publicUrl,
278
- adminPassword,
279
- cliTokenService,
280
- externalAuthClient: coreServices.getExternalAuthClient(),
281
- agentSettingsStore: coreServices.getAgentSettingsStore(),
282
- agentConfigStore: coreServices.getConfigStore(),
283
- platformRegistry,
284
- });
285
- app.route("", agentApi);
286
- logger.debug(
287
- "Agent API enabled at :8080/api/v1/agents/* with docs at :8080/api/docs"
288
- );
289
- }
290
- }
291
-
292
- if (coreServices) {
293
- // Mount OAuth modules under unified auth router
294
- const authRouter = new OpenAPIHono();
295
- const registeredProviders: string[] = [];
296
-
297
- {
298
- const {
299
- createCliAuthRoutes,
300
- createConnectAuthRoutes,
301
- } = require("../routes/public/cli-auth");
302
- const cliAuthRouter = createCliAuthRoutes({
303
- queue: coreServices.getQueue(),
304
- externalAuthClient: coreServices.getExternalAuthClient(),
305
- allowAdminPasswordLogin: process.env.NODE_ENV !== "production",
306
- adminPassword,
307
- });
308
- const connectAuthRouter = createConnectAuthRoutes({
309
- queue: coreServices.getQueue(),
310
- externalAuthClient: coreServices.getExternalAuthClient(),
311
- allowAdminPasswordLogin: process.env.NODE_ENV !== "production",
312
- adminPassword,
313
- });
314
- authRouter.route("", cliAuthRouter);
315
- app.route("", connectAuthRouter);
316
- registeredProviders.push("cli-auth");
317
- }
318
-
319
- const providerModules = getModelProviderModules();
320
-
321
- const authProfilesManager = coreServices.getAuthProfilesManager();
322
- if (authProfilesManager) {
323
- const {
324
- verifySettingsSessionOrToken,
325
- } = require("../routes/public/settings-auth");
326
- const {
327
- createAuthProfileLabel,
328
- } = require("../auth/settings/auth-profiles-manager");
329
- const agentMetadataStore = coreServices.getAgentMetadataStore();
330
- const userAgentsStore = coreServices.getUserAgentsStore();
331
-
332
- const verifyProviderAuth = async (
333
- c: any,
334
- agentId: string
335
- ): Promise<boolean> => {
336
- const payload = verifySettingsSessionOrToken(c);
337
- if (!payload) return false;
338
- if (payload.isAdmin) return true;
339
-
340
- if (payload.agentId) return payload.agentId === agentId;
341
-
342
- if (userAgentsStore) {
343
- const owns = await userAgentsStore.ownsAgent(
344
- payload.platform,
345
- payload.userId,
346
- agentId
347
- );
348
- if (owns) return true;
349
- }
350
-
351
- if (agentMetadataStore) {
352
- const metadata = await agentMetadataStore.getMetadata(agentId);
353
- const isOwner =
354
- metadata?.owner?.platform === payload.platform &&
355
- metadata?.owner?.userId === payload.userId;
356
- if (isOwner) {
357
- userAgentsStore
358
- ?.addAgent(payload.platform, payload.userId, agentId)
359
- .catch(() => {
360
- /* best-effort reconciliation */
361
- });
362
- return true;
363
- }
364
- }
365
-
366
- return false;
367
- };
368
-
369
- authRouter.post("/:provider/save-key", async (c: any) => {
370
- try {
371
- const providerId = c.req.param("provider");
372
- const mod = getModelProviderModules().find(
373
- (m) => m.providerId === providerId
374
- );
375
- if (!mod) return c.json({ error: "Unknown provider" }, 404);
376
-
377
- const body = await c.req.json();
378
- const { agentId, apiKey } = body;
379
- if (!agentId || !apiKey) {
380
- return c.json({ error: "Missing agentId or apiKey" }, 400);
381
- }
382
-
383
- if (!(await verifyProviderAuth(c, agentId))) {
384
- return c.json({ error: "Unauthorized" }, 401);
385
- }
386
-
387
- await authProfilesManager.upsertProfile({
388
- agentId,
389
- provider: providerId,
390
- credential: apiKey,
391
- authType: "api-key",
392
- label: createAuthProfileLabel(mod.providerDisplayName, apiKey),
393
- makePrimary: true,
394
- });
395
-
396
- return c.json({ success: true });
397
- } catch (error) {
398
- logger.error("Failed to save API key", { error });
399
- return c.json({ error: "Failed to save API key" }, 500);
400
- }
401
- });
402
-
403
- authRouter.post("/:provider/start", async (c: any) => {
404
- try {
405
- const providerId = c.req.param("provider");
406
- const mod = getModelProviderModules().find(
407
- (m) => m.providerId === providerId
408
- );
409
- if (!mod) return c.json({ error: "Unknown provider" }, 404);
410
-
411
- const supportsDeviceCode =
412
- mod.authType === "device-code" ||
413
- mod.supportedAuthTypes?.includes("device-code");
414
- if (!supportsDeviceCode) {
415
- return c.json(
416
- { error: "Provider does not support device code" },
417
- 400
418
- );
419
- }
420
-
421
- if (typeof mod.startDeviceCode !== "function") {
422
- return c.json({ error: "Device code start not implemented" }, 501);
423
- }
424
-
425
- const body = (await c.req.json().catch(() => ({}))) as {
426
- agentId?: string;
427
- };
428
- const agentId = body.agentId?.trim();
429
- if (!agentId) return c.json({ error: "Missing agentId" }, 400);
430
-
431
- if (!(await verifyProviderAuth(c, agentId))) {
432
- return c.json({ error: "Unauthorized" }, 401);
433
- }
434
-
435
- const result = await mod.startDeviceCode(agentId);
436
- return c.json(result);
437
- } catch (error) {
438
- logger.error("Failed to start device code flow", { error });
439
- return c.json({ error: "Failed to start device code flow" }, 500);
440
- }
441
- });
442
-
443
- authRouter.post("/:provider/poll", async (c: any) => {
444
- try {
445
- const providerId = c.req.param("provider");
446
- const mod = getModelProviderModules().find(
447
- (m) => m.providerId === providerId
448
- );
449
- if (!mod) return c.json({ error: "Unknown provider" }, 404);
450
-
451
- const supportsDeviceCode =
452
- mod.authType === "device-code" ||
453
- mod.supportedAuthTypes?.includes("device-code");
454
- if (!supportsDeviceCode) {
455
- return c.json(
456
- { error: "Provider does not support device code" },
457
- 400
458
- );
459
- }
460
-
461
- if (typeof mod.pollDeviceCode !== "function") {
462
- return c.json({ error: "Device code poll not implemented" }, 501);
463
- }
464
-
465
- const body = (await c.req.json().catch(() => ({}))) as {
466
- agentId?: string;
467
- deviceAuthId?: string;
468
- userCode?: string;
469
- };
470
- const agentId = body.agentId?.trim();
471
- const deviceAuthId = body.deviceAuthId?.trim();
472
- const userCode = body.userCode?.trim();
473
- if (!agentId || !deviceAuthId || !userCode) {
474
- return c.json(
475
- { error: "Missing agentId, deviceAuthId, or userCode" },
476
- 400
477
- );
478
- }
479
-
480
- if (!(await verifyProviderAuth(c, agentId))) {
481
- return c.json({ error: "Unauthorized" }, 401);
482
- }
483
-
484
- const result = await mod.pollDeviceCode(agentId, {
485
- deviceAuthId,
486
- userCode,
487
- });
488
- return c.json(result);
489
- } catch (error) {
490
- logger.error("Failed to poll device code flow", { error });
491
- return c.json({ error: "Failed to poll device code flow" }, 500);
492
- }
493
- });
494
-
495
- authRouter.post("/:provider/logout", async (c: any) => {
496
- try {
497
- const providerId = c.req.param("provider");
498
- const mod = getModelProviderModules().find(
499
- (m) => m.providerId === providerId
500
- );
501
- if (!mod) return c.json({ error: "Unknown provider" }, 404);
502
-
503
- const body = await c.req.json().catch(() => ({}));
504
- const agentId = body.agentId || c.req.query("agentId");
505
- if (!agentId) {
506
- return c.json({ error: "Missing agentId" }, 400);
507
- }
508
-
509
- if (!(await verifyProviderAuth(c, agentId))) {
510
- return c.json({ error: "Unauthorized" }, 401);
511
- }
512
-
513
- await authProfilesManager.deleteProviderProfiles(
514
- agentId,
515
- providerId,
516
- body.profileId
517
- );
518
-
519
- return c.json({ success: true });
520
- } catch (error) {
521
- logger.error("Failed to logout", { error });
522
- return c.json({ error: "Failed to logout" }, 500);
523
- }
524
- });
525
- }
526
-
527
- // Get shared dependencies (needed before mounting auth router)
528
- const agentSettingsStore = coreServices.getAgentSettingsStore();
529
- const claudeOAuthStateStore = coreServices.getOAuthStateStore();
530
- const scheduledWakeupService = coreServices.getScheduledWakeupService();
531
-
532
- // Build provider stores and overrides dynamically from registered modules
533
- const providerStores: Record<
534
- string,
535
- { hasCredentials(agentId: string): Promise<boolean> }
536
- > = {};
537
- const providerConnectedOverrides: Record<string, boolean> = {};
538
- for (const mod of providerModules) {
539
- providerStores[mod.providerId] = mod;
540
- providerConnectedOverrides[mod.providerId] = mod.hasSystemKey();
541
- if (mod.getApp) {
542
- authRouter.route(`/${mod.providerId}`, mod.getApp());
543
- registeredProviders.push(mod.providerId);
544
- }
545
- }
546
-
547
- const systemSkillsService = coreServices.getSystemSkillsService();
548
-
549
- if (systemSkillsService) {
550
- const { SystemEnvStore } = require("../auth/system-env-store");
551
- const { setEnvResolver } = require("../auth/mcp/string-substitution");
552
- const systemEnvStore = new SystemEnvStore(
553
- coreServices.getQueue().getRedisClient()
554
- );
555
- systemEnvStore.refreshCache().catch((e: any) => {
556
- logger.error("Failed to refresh system env cache", { error: e });
557
- });
558
- setEnvResolver((key: string) => systemEnvStore.resolve(key));
559
- }
560
-
561
- if (!process.env.ADMIN_PASSWORD) {
562
- logger.info(
563
- "An admin password has been auto-generated. For security reasons, it is not logged. Set the ADMIN_PASSWORD env var to use a fixed password."
564
- );
565
- }
566
-
567
- // Landing page (docs + integrations)
568
- {
569
- const { createLandingRoutes } = require("../routes/public/landing");
570
- const landingRouter = createLandingRoutes();
571
- app.route("", landingRouter);
572
- logger.debug("Landing page enabled at :8080/");
573
- }
574
-
575
- // Agent history routes (proxy to worker HTTP server)
576
- {
577
- const connectionManager = coreServices
578
- .getWorkerGateway()
579
- ?.getConnectionManager();
580
- if (connectionManager) {
581
- const {
582
- createAgentHistoryRoutes,
583
- } = require("../routes/public/agent-history");
584
- const agentHistoryRouter = createAgentHistoryRoutes({
585
- connectionManager,
586
- chatInstanceManager: chatInstanceManager ?? undefined,
587
- agentConfigStore: coreServices.getConfigStore(),
588
- userAgentsStore: coreServices.getUserAgentsStore(),
589
- });
590
- app.route("/api/v1/agents/:agentId/history", agentHistoryRouter);
591
- logger.debug(
592
- "Agent history routes enabled at :8080/api/v1/agents/{agentId}/history/*"
593
- );
594
- }
595
- }
596
-
597
- // Agent config routes (/api/v1/agents/{id}/config)
598
- if (agentSettingsStore) {
599
- const {
600
- createAgentConfigRoutes,
601
- } = require("../routes/public/agent-config");
602
-
603
- const agentConfigRouter = createAgentConfigRoutes({
604
- agentSettingsStore,
605
- agentConfigStore: coreServices.getConfigStore()!,
606
- userAgentsStore: coreServices.getUserAgentsStore(),
607
- queue: coreServices.getQueue(),
608
- providerStores:
609
- Object.keys(providerStores).length > 0 ? providerStores : undefined,
610
- providerConnectedOverrides,
611
- providerCatalogService: coreServices.getProviderCatalogService(),
612
- authProfilesManager: coreServices.getAuthProfilesManager(),
613
- connectionManager: coreServices
614
- .getWorkerGateway()
615
- ?.getConnectionManager(),
616
- grantStore: coreServices.getGrantStore(),
617
- scheduledWakeupService: coreServices.getScheduledWakeupService(),
618
- });
619
- app.route("/api/v1/agents/:agentId/config", agentConfigRouter);
620
- logger.debug(
621
- "Agent config routes enabled at :8080/api/v1/agents/{id}/config"
622
- );
623
- }
624
-
625
- // Agent schedules routes (/api/v1/agents/{id}/schedules)
626
- {
627
- const {
628
- createAgentSchedulesRoutes,
629
- } = require("../routes/public/agent-schedules");
630
- const agentSchedulesRouter = createAgentSchedulesRoutes({
631
- scheduledWakeupService,
632
- externalAuthClient: coreServices.getExternalAuthClient(),
633
- userAgentsStore: coreServices.getUserAgentsStore(),
634
- agentMetadataStore: coreServices.getConfigStore(),
635
- });
636
- app.route("/api/v1/agents/:agentId/schedules", agentSchedulesRouter);
637
- logger.debug(
638
- "Agent schedules routes enabled at :8080/api/v1/agents/{id}/schedules"
639
- );
640
- }
641
-
642
- // OAuth routes (mounted under unified auth router)
643
- if (agentSettingsStore) {
644
- const { createOAuthRoutes } = require("../routes/public/oauth");
645
- const { OAuthClient } = require("../auth/oauth/client");
646
- const { CLAUDE_PROVIDER } = require("../auth/oauth/providers");
647
- const claudeOAuthClient = new OAuthClient(CLAUDE_PROVIDER);
648
- const oauthRouter = createOAuthRoutes({
649
- providerStores:
650
- Object.keys(providerStores).length > 0 ? providerStores : undefined,
651
- oauthClients: { claude: claudeOAuthClient },
652
- oauthStateStore: claudeOAuthStateStore,
653
- });
654
- authRouter.route("", oauthRouter);
655
- registeredProviders.push("oauth");
656
- }
657
-
658
- // Mount unified auth router (includes provider modules + OAuth)
659
- if (registeredProviders.length > 0) {
660
- app.route("/api/v1/auth", authRouter);
661
- logger.debug(
662
- `Auth routes enabled at :8080/api/v1/auth/* for: ${registeredProviders.join(", ")}`
663
- );
664
- }
665
-
666
- // Channel binding routes (mount under agent API)
667
- const channelBindingService = coreServices.getChannelBindingService();
668
- if (channelBindingService) {
669
- const {
670
- createChannelBindingRoutes,
671
- } = require("../routes/public/channels");
672
- const channelBindingRouter = createChannelBindingRoutes({
673
- channelBindingService,
674
- userAgentsStore: coreServices.getUserAgentsStore(),
675
- agentMetadataStore: coreServices.getAgentMetadataStore(),
676
- });
677
- // Mount as a sub-router under /api/v1/agents/:agentId/channels
678
- app.route("/api/v1/agents/:agentId/channels", channelBindingRouter);
679
- logger.debug(
680
- "Channel binding routes enabled at :8080/api/v1/agents/{agentId}/channels/*"
681
- );
682
- }
683
-
684
- // Agent management routes (separate from Agent API's /api/v1/agents)
685
- {
686
- const userAgentsStore = coreServices.getUserAgentsStore();
687
- const agentMetadataStore = coreServices.getAgentMetadataStore();
688
- const { createAgentRoutes } = require("../routes/public/agents");
689
- const agentManagementRouter = createAgentRoutes({
690
- userAgentsStore,
691
- agentMetadataStore,
692
- agentSettingsStore,
693
- channelBindingService,
694
- });
695
- app.route("/api/v1/agents", agentManagementRouter);
696
- logger.debug("Agent management routes enabled at :8080/api/v1/agents/*");
697
- }
698
- }
699
-
700
- // Chat SDK connection routes (webhook + CRUD)
701
- if (chatInstanceManager) {
702
- const {
703
- createSlackRoutes,
704
- createConnectionWebhookRoutes,
705
- createConnectionCrudRoutes,
706
- } = {
707
- ...require("../routes/public/slack"),
708
- ...require("../routes/public/connections"),
709
- };
710
- app.route("", createSlackRoutes(chatInstanceManager));
711
- app.route("", createConnectionWebhookRoutes(chatInstanceManager));
712
- app.route(
713
- "",
714
- createConnectionCrudRoutes(chatInstanceManager, {
715
- userAgentsStore: coreServices.getUserAgentsStore(),
716
- agentMetadataStore: coreServices.getConfigStore()!,
717
- })
718
- );
719
- logger.debug(
720
- "Slack and connection routes enabled at :8080/slack/*, :8080/api/v1/connections/*, and :8080/api/v1/webhooks/*"
721
- );
722
- }
723
-
724
- // ─── Reload endpoint (file-first dev mode) ──────────────────────────────────
725
- // Re-reads lobu.toml + markdown and re-populates InMemoryAgentStore.
726
- // Only works in dev mode (files exist), authenticated with ADMIN_PASSWORD.
727
- app.post("/api/v1/reload", async (c) => {
728
- if (process.env.NODE_ENV === "production") {
729
- return c.json({ error: "Not found" }, 404);
730
- }
731
- const authHeader = c.req.header("Authorization");
732
- if (authHeader !== `Bearer ${adminPassword}`) {
733
- return c.json({ error: "Unauthorized" }, 401);
734
- }
735
-
736
- if (!coreServices?.isFileFirstMode()) {
737
- return c.json(
738
- { error: "Reload only available in file-first dev mode" },
739
- 400
740
- );
741
- }
742
-
743
- try {
744
- const result = await coreServices.reloadFromFiles();
745
- return c.json(result);
746
- } catch (err) {
747
- logger.error("Reload failed", {
748
- error: err instanceof Error ? err.message : String(err),
749
- });
750
- return c.json({ error: "Reload failed" }, 500);
751
- }
752
- });
753
-
754
- // ─── Internal CLI status endpoint ──────────────────────────────────────────
755
- // Returns agents, connections, and sandboxes for `lobu status`.
756
- // Only available in non-production, authenticated with ADMIN_PASSWORD.
757
- app.get("/internal/status", async (c) => {
758
- if (process.env.NODE_ENV === "production") {
759
- return c.json({ error: "Not found" }, 404);
760
- }
761
- const authHeader = c.req.header("Authorization");
762
- if (authHeader !== `Bearer ${adminPassword}`) {
763
- return c.json({ error: "Unauthorized" }, 401);
764
- }
765
-
766
- const agentConfigStore = coreServices?.getConfigStore();
767
-
768
- const allAgents: AgentMetadata[] = agentConfigStore
769
- ? await agentConfigStore.listAgents()
770
- : [];
771
- const templateAgents = allAgents.filter(
772
- (a: AgentMetadata) => !a.parentConnectionId
773
- );
774
- const sandboxAgents = allAgents.filter(
775
- (a: AgentMetadata) => !!a.parentConnectionId
776
- );
777
-
778
- const connections = chatInstanceManager
779
- ? await chatInstanceManager.listConnections()
780
- : [];
781
-
782
- const agentDetails = [];
783
- for (const a of templateAgents) {
784
- const settings = agentConfigStore
785
- ? await agentConfigStore.getSettings(a.agentId)
786
- : null;
787
- const providers = (settings?.installedProviders || []).map(
788
- (p: { providerId: string }) => p.providerId
789
- );
790
- agentDetails.push({
791
- agentId: a.agentId,
792
- name: a.name,
793
- providers,
794
- model:
795
- settings?.modelSelection?.mode === "pinned"
796
- ? (settings.modelSelection as { pinnedModel?: string })
797
- .pinnedModel || "pinned"
798
- : settings?.modelSelection?.mode || "auto",
799
- });
800
- }
801
-
802
- return c.json({
803
- agents: agentDetails,
804
- connections: connections.map(
805
- (conn: {
806
- id: string;
807
- platform: string;
808
- templateAgentId?: string;
809
- metadata?: Record<string, string>;
810
- }) => ({
811
- id: conn.id,
812
- platform: conn.platform,
813
- status: chatInstanceManager?.getInstance(conn.id)
814
- ? "connected"
815
- : "disconnected",
816
- templateAgentId: conn.templateAgentId || null,
817
- botUsername: conn.metadata?.botUsername || null,
818
- })
819
- ),
820
- sandboxes: sandboxAgents.map((s: AgentMetadata) => ({
821
- agentId: s.agentId,
822
- name: s.name,
823
- parentConnectionId: s.parentConnectionId || null,
824
- lastUsedAt: s.lastUsedAt ?? null,
825
- })),
826
- });
827
- });
828
-
829
- // Auto-register any non-openapi routes so everything shows up in the schema
830
- registerAutoOpenApiRoutes(app);
831
-
832
- // OpenAPI Documentation
833
- app.doc("/api/docs/openapi.json", {
834
- openapi: "3.0.0",
835
- info: {
836
- title: "Lobu API",
837
- version: "1.0.0",
838
- description: `
839
- ## Overview
840
-
841
- The Lobu API allows you to create and interact with AI agents programmatically.
842
-
843
- ## Authentication
844
-
845
- 1. Authenticate the agent-creation request with an admin password or CLI access token
846
- 2. Create an agent with \`POST /api/v1/agents\` to get a worker token
847
- 3. Use the returned worker token as a Bearer token for subsequent agent requests
848
-
849
- ## Quick Start
850
-
851
- \`\`\`bash
852
- # 1. Create an agent (authenticate with admin password or CLI token)
853
- curl -X POST http://localhost:8080/api/v1/agents \\
854
- -H "Authorization: Bearer $ADMIN_PASSWORD" \\
855
- -H "Content-Type: application/json" \\
856
- -d '{"provider": "claude"}'
857
-
858
- # 2. Send a message (use worker token from step 1)
859
- curl -X POST http://localhost:8080/api/v1/agents/{agentId}/messages \\
860
- -H "Authorization: Bearer {token}" \\
861
- -H "Content-Type: application/json" \\
862
- -d '{"content": "Hello!"}'
863
- \`\`\`
864
-
865
- ## MCP Servers
866
-
867
- Agents can be configured with custom MCP (Model Context Protocol) servers:
868
-
869
- \`\`\`json
870
- {
871
- "mcpServers": {
872
- "my-http-mcp": { "url": "https://my-mcp.com/sse" },
873
- "my-stdio-mcp": { "command": "npx", "args": ["-y", "@org/mcp"] }
874
- }
875
- }
876
- \`\`\`
877
- `,
878
- },
879
- tags: [
880
- {
881
- name: "Agents",
882
- description: "Create, list, update, and delete agents.",
883
- },
884
- {
885
- name: "Messages",
886
- description:
887
- "Send messages to agents and subscribe to real-time events (SSE).",
888
- },
889
- {
890
- name: "Configuration",
891
- description:
892
- "Agent configuration — LLM providers, Nix packages, domain grants.",
893
- },
894
- {
895
- name: "Channels",
896
- description:
897
- "Bind agents to messaging platform channels (Slack, Telegram, WhatsApp).",
898
- },
899
- {
900
- name: "Connections",
901
- description:
902
- "Manage Chat SDK-backed platform connections and their lifecycle.",
903
- },
904
- {
905
- name: "Schedules",
906
- description: "Scheduled wakeups and recurring reminders.",
907
- },
908
- {
909
- name: "History",
910
- description: "Session messages, stats, and connection status.",
911
- },
912
- {
913
- name: "Auth",
914
- description:
915
- "Provider authentication — API keys, OAuth, device code flows.",
916
- },
917
- {
918
- name: "Integrations",
919
- description: "Browse and install skills and MCP servers.",
920
- },
921
- ],
922
- servers: [
923
- { url: "http://localhost:8080", description: "Local development" },
924
- ],
925
- });
926
-
927
- app.get(
928
- "/api/docs",
929
- apiReference({
930
- url: "/api/docs/openapi.json",
931
- theme: "kepler",
932
- layout: "modern",
933
- defaultHttpClient: { targetKey: "js", clientKey: "fetch" },
934
- })
935
- );
936
- logger.debug("API docs enabled at /api/docs");
937
-
938
- return app;
939
- }
940
-
941
- /**
942
- * Start an HTTP server for the gateway Hono app.
943
- * Used in standalone mode. In embedded mode, the host creates its own server.
944
- */
945
- export function startGatewayServer(app: OpenAPIHono, port = 8080): Server {
946
- const honoListener = getRequestListener(app.fetch);
947
- const server = createServer(honoListener);
948
- server.listen(port);
949
- logger.debug(`Server listening on port ${port}`);
950
- return server;
951
- }
952
-
953
- /**
954
- * Handle Express-style handler with Hono context
955
- */
956
- async function handleExpressHandler(c: any, handler: any): Promise<Response> {
957
- const { req, res, responsePromise } = createExpressCompatObjects(c);
958
- await handler(req, res);
959
- return responsePromise;
960
- }
961
-
962
- /**
963
- * Create Express-compatible request/response objects from Hono context
964
- */
965
- function createExpressCompatObjects(c: any, overridePath?: string) {
966
- let resolveResponse: (response: Response) => void;
967
- const responsePromise = new Promise<Response>((resolve) => {
968
- resolveResponse = resolve;
969
- });
970
-
971
- const url = new URL(c.req.url);
972
- const headers: Record<string, string> = {};
973
- c.req.raw.headers.forEach((value: string, key: string) => {
974
- headers[key] = value;
975
- });
976
-
977
- // Express-compatible request object
978
- const req: any = {
979
- method: c.req.method,
980
- url: c.req.url,
981
- path: overridePath || url.pathname,
982
- headers,
983
- query: Object.fromEntries(url.searchParams),
984
- params: c.req.param() || {},
985
- body: null,
986
- get: (name: string) => headers[name.toLowerCase()],
987
- on: () => {
988
- // Express event listener stub - not used in Hono compat layer
989
- },
990
- };
991
-
992
- // Response state
993
- let statusCode = 200;
994
- const responseHeaders = new Headers();
995
- let isStreaming = false;
996
- let streamController: ReadableStreamDefaultController<Uint8Array> | null =
997
- null;
998
-
999
- // Express-compatible response object
1000
- const res: any = {
1001
- statusCode: 200,
1002
- destroyed: false,
1003
- writableEnded: false,
1004
-
1005
- status(code: number) {
1006
- statusCode = code;
1007
- this.statusCode = code;
1008
- return this;
1009
- },
1010
-
1011
- setHeader(name: string, value: string) {
1012
- responseHeaders.set(name, value);
1013
- return this;
1014
- },
1015
-
1016
- set(name: string, value: string) {
1017
- responseHeaders.set(name, value);
1018
- return this;
1019
- },
1020
-
1021
- json(data: any) {
1022
- responseHeaders.set("Content-Type", "application/json");
1023
- resolveResponse?.(
1024
- new Response(JSON.stringify(data), {
1025
- status: statusCode,
1026
- headers: responseHeaders,
1027
- })
1028
- );
1029
- },
1030
-
1031
- send(data: any) {
1032
- resolveResponse?.(
1033
- new Response(data, {
1034
- status: statusCode,
1035
- headers: responseHeaders,
1036
- })
1037
- );
1038
- },
1039
-
1040
- text(data: string) {
1041
- resolveResponse?.(
1042
- new Response(data, {
1043
- status: statusCode,
1044
- headers: responseHeaders,
1045
- })
1046
- );
1047
- },
1048
-
1049
- end(data?: any) {
1050
- this.writableEnded = true;
1051
- if (isStreaming && streamController) {
1052
- if (data) {
1053
- streamController.enqueue(
1054
- typeof data === "string" ? new TextEncoder().encode(data) : data
1055
- );
1056
- }
1057
- streamController.close();
1058
- } else {
1059
- resolveResponse?.(
1060
- new Response(data || null, {
1061
- status: statusCode,
1062
- headers: responseHeaders,
1063
- })
1064
- );
1065
- }
1066
- },
1067
-
1068
- write(chunk: any) {
1069
- if (!isStreaming) {
1070
- isStreaming = true;
1071
- const stream = new ReadableStream({
1072
- start(controller) {
1073
- streamController = controller;
1074
- if (chunk) {
1075
- controller.enqueue(
1076
- typeof chunk === "string"
1077
- ? new TextEncoder().encode(chunk)
1078
- : chunk
1079
- );
1080
- }
1081
- },
1082
- });
1083
- resolveResponse?.(
1084
- new Response(stream, {
1085
- status: statusCode,
1086
- headers: responseHeaders,
1087
- })
1088
- );
1089
- } else if (streamController) {
1090
- streamController.enqueue(
1091
- typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk
1092
- );
1093
- }
1094
- return true;
1095
- },
1096
-
1097
- flushHeaders() {
1098
- // No-op for compatibility
1099
- },
1100
- };
1101
-
1102
- // Parse body for POST/PUT/PATCH
1103
- if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
1104
- const contentType = c.req.header("content-type") || "";
1105
- c.req.raw
1106
- .clone()
1107
- .arrayBuffer()
1108
- .then((buffer: ArrayBuffer) => {
1109
- if (contentType.includes("application/json")) {
1110
- try {
1111
- req.body = JSON.parse(new TextDecoder().decode(buffer));
1112
- } catch {
1113
- req.body = buffer;
1114
- }
1115
- } else {
1116
- req.body = buffer;
1117
- }
1118
- });
1119
- }
1120
-
1121
- return { req, res, responsePromise };
1122
- }
1123
-
1124
- /**
1125
- * Create Express-like adapter for compatibility with module registry
1126
- */
1127
- function createExpressAdapter(honoApp: any) {
1128
- return {
1129
- get: (path: string, ...handlers: any[]) => {
1130
- const handler = handlers[handlers.length - 1];
1131
- honoApp.get(path, (c: any) => handleExpressHandler(c, handler));
1132
- },
1133
- post: (path: string, ...handlers: any[]) => {
1134
- const handler = handlers[handlers.length - 1];
1135
- honoApp.post(path, (c: any) => handleExpressHandler(c, handler));
1136
- },
1137
- put: (path: string, ...handlers: any[]) => {
1138
- const handler = handlers[handlers.length - 1];
1139
- honoApp.put(path, (c: any) => handleExpressHandler(c, handler));
1140
- },
1141
- delete: (path: string, ...handlers: any[]) => {
1142
- const handler = handlers[handlers.length - 1];
1143
- honoApp.delete(path, (c: any) => handleExpressHandler(c, handler));
1144
- },
1145
- use: (pathOrHandler: any, handler?: any) => {
1146
- if (typeof pathOrHandler === "function") {
1147
- // Global middleware - skip for now
1148
- } else if (handler) {
1149
- honoApp.all(`${pathOrHandler}/*`, (c: any) =>
1150
- handleExpressHandler(c, handler)
1151
- );
1152
- }
1153
- },
1154
- };
1155
- }
1156
-
1157
- /**
1158
- * Start the gateway with the provided configuration
1159
- */
1160
- export async function startGateway(config: GatewayConfig): Promise<void> {
1161
- logger.info("Starting Lobu Gateway");
1162
-
1163
- // Start filtering proxy for worker network isolation (if enabled)
1164
- const { startFilteringProxy } = await import("../proxy/proxy-manager");
1165
- await startFilteringProxy();
1166
-
1167
- // Import dependencies
1168
- const { Orchestrator } = await import("../orchestration");
1169
- const { Gateway } = await import("../gateway-main");
1170
-
1171
- // Create and start orchestrator
1172
- logger.debug("Creating orchestrator", { mode: process.env.DEPLOYMENT_MODE });
1173
- const orchestrator = new Orchestrator(config.orchestration);
1174
- await orchestrator.start();
1175
- logger.debug("Orchestrator started");
1176
-
1177
- // Create Gateway
1178
- const gateway = new Gateway(config);
1179
-
1180
- // Register API platform (always enabled)
1181
- const { ApiPlatform } = await import("../api");
1182
- const apiPlatform = new ApiPlatform();
1183
- gateway.registerPlatform(apiPlatform);
1184
- logger.debug("API platform registered");
1185
-
1186
- // Start gateway
1187
- await gateway.start();
1188
- logger.debug("Gateway started");
1189
-
1190
- // Get core services
1191
- const coreServices = gateway.getCoreServices();
1192
-
1193
- // Wire grant store to HTTP proxy for domain grant checks
1194
- const grantStore = coreServices.getGrantStore();
1195
- if (grantStore) {
1196
- const { setProxyGrantStore } = await import("../proxy/http-proxy");
1197
- setProxyGrantStore(grantStore);
1198
- logger.debug("Grant store connected to HTTP proxy");
1199
- }
1200
-
1201
- // Inject core services into orchestrator (provider modules carry their own credential stores)
1202
- await orchestrator.injectCoreServices(
1203
- coreServices.getQueue().getRedisClient(),
1204
- coreServices.getProviderCatalogService(),
1205
- coreServices.getGrantStore() ?? undefined
1206
- );
1207
- logger.debug("Orchestrator configured with core services");
1208
-
1209
- // Initialize Chat SDK connection manager (API-driven platform connections)
1210
- const { ChatInstanceManager, ChatResponseBridge } = await import(
1211
- "../connections"
1212
- );
1213
- const chatInstanceManager = new ChatInstanceManager();
1214
- try {
1215
- await chatInstanceManager.initialize(coreServices);
1216
-
1217
- // Register chat platform adapters (delegates to ChatInstanceManager)
1218
- for (const adapter of chatInstanceManager.createPlatformAdapters()) {
1219
- gateway.registerPlatform(adapter);
1220
- }
1221
- logger.debug("ChatInstanceManager initialized");
1222
-
1223
- // Seed connections from file-loaded agents (file-first architecture)
1224
- const fileLoadedAgents = coreServices.getFileLoadedAgents();
1225
- if (fileLoadedAgents.length > 0) {
1226
- for (const agent of fileLoadedAgents) {
1227
- if (!agent.connections?.length) continue;
1228
- for (const conn of agent.connections) {
1229
- const existing = await chatInstanceManager.listConnections({
1230
- platform: conn.type,
1231
- templateAgentId: agent.agentId,
1232
- });
1233
- if (existing.length > 0) continue;
1234
- try {
1235
- await chatInstanceManager.addConnection(
1236
- conn.type,
1237
- agent.agentId,
1238
- { platform: conn.type as any, ...conn.config },
1239
- { allowGroups: true }
1240
- );
1241
- logger.debug(
1242
- `Created ${conn.type} connection for agent "${agent.agentId}"`
1243
- );
1244
- } catch (err) {
1245
- logger.error(
1246
- `Failed to create ${conn.type} connection for agent "${agent.agentId}"`,
1247
- { error: err instanceof Error ? err.message : String(err) }
1248
- );
1249
- }
1250
- }
1251
- }
1252
- }
1253
-
1254
- // Wire ChatResponseBridge into unified thread consumer
1255
- const unifiedConsumer = gateway.getUnifiedConsumer();
1256
- if (unifiedConsumer) {
1257
- const chatResponseBridge = new ChatResponseBridge(chatInstanceManager);
1258
- unifiedConsumer.setChatResponseBridge(chatResponseBridge);
1259
- logger.debug("ChatResponseBridge wired to unified thread consumer");
1260
- }
1261
- } catch (error) {
1262
- logger.warn(
1263
- { error: String(error) },
1264
- "ChatInstanceManager initialization failed — connections feature disabled"
1265
- );
1266
- }
1267
-
1268
- // Setup server on port 8080 (single port for all HTTP traffic)
1269
- if (!httpServer) {
1270
- const app = createGatewayApp({
1271
- secretProxy: coreServices.getSecretProxy(),
1272
- workerGateway: coreServices.getWorkerGateway(),
1273
- mcpProxy: coreServices.getMcpProxy(),
1274
- interactionService: coreServices.getInteractionService(),
1275
- platformRegistry: gateway.getPlatformRegistry(),
1276
- coreServices,
1277
- chatInstanceManager,
1278
- });
1279
- httpServer = startGatewayServer(app);
1280
- }
1281
-
1282
- logger.info("Lobu Gateway is running!");
1283
-
1284
- // Setup graceful shutdown
1285
- const cleanup = async () => {
1286
- logger.info("Shutting down gateway...");
1287
-
1288
- // Hard deadline: force exit after 30s if graceful shutdown stalls
1289
- const hardDeadline = setTimeout(() => {
1290
- logger.error("Graceful shutdown timed out after 30s, forcing exit");
1291
- process.exit(1);
1292
- }, 30_000);
1293
- hardDeadline.unref();
1294
-
1295
- await chatInstanceManager.shutdown();
1296
- await orchestrator.stop();
1297
- await gateway.stop();
1298
- if (httpServer) {
1299
- httpServer.close();
1300
- }
1301
- logger.info("Gateway shutdown complete");
1302
- process.exit(0);
1303
- };
1304
-
1305
- process.on("SIGINT", cleanup);
1306
- process.on("SIGTERM", cleanup);
1307
-
1308
- process.on("SIGUSR1", () => {
1309
- const status = gateway.getStatus();
1310
- logger.info("Health check:", JSON.stringify(status, null, 2));
1311
- });
1312
- }