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