@lobu/gateway 3.0.9 → 3.0.12

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 (210) 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/interactions.d.ts +9 -43
  19. package/dist/interactions.d.ts.map +1 -1
  20. package/dist/interactions.js +10 -52
  21. package/dist/interactions.js.map +1 -1
  22. package/dist/routes/public/agent.d.ts +4 -0
  23. package/dist/routes/public/agent.d.ts.map +1 -1
  24. package/dist/routes/public/agent.js +21 -0
  25. package/dist/routes/public/agent.js.map +1 -1
  26. package/dist/services/core-services.d.ts.map +1 -1
  27. package/dist/services/core-services.js +4 -0
  28. package/dist/services/core-services.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/__tests__/agent-config-routes.test.ts +0 -254
  31. package/src/__tests__/agent-history-routes.test.ts +0 -72
  32. package/src/__tests__/agent-routes.test.ts +0 -68
  33. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  34. package/src/__tests__/agent-settings-store.test.ts +0 -323
  35. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  36. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  37. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  38. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  39. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  40. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  41. package/src/__tests__/config-request-store.test.ts +0 -127
  42. package/src/__tests__/connection-routes.test.ts +0 -144
  43. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  44. package/src/__tests__/docker-deployment.test.ts +0 -1211
  45. package/src/__tests__/embedded-deployment.test.ts +0 -342
  46. package/src/__tests__/grant-store.test.ts +0 -148
  47. package/src/__tests__/http-proxy.test.ts +0 -281
  48. package/src/__tests__/instruction-service.test.ts +0 -37
  49. package/src/__tests__/link-buttons.test.ts +0 -112
  50. package/src/__tests__/lobu.test.ts +0 -32
  51. package/src/__tests__/mcp-config-service.test.ts +0 -347
  52. package/src/__tests__/mcp-proxy.test.ts +0 -694
  53. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  54. package/src/__tests__/model-selection.test.ts +0 -172
  55. package/src/__tests__/oauth-templates.test.ts +0 -39
  56. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  57. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  58. package/src/__tests__/provider-inheritance.test.ts +0 -212
  59. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  60. package/src/__tests__/routes/interactions.test.ts +0 -121
  61. package/src/__tests__/secret-proxy.test.ts +0 -85
  62. package/src/__tests__/session-manager.test.ts +0 -572
  63. package/src/__tests__/setup.ts +0 -133
  64. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  65. package/src/__tests__/slack-routes.test.ts +0 -161
  66. package/src/__tests__/system-config-resolver.test.ts +0 -75
  67. package/src/__tests__/system-message-limiter.test.ts +0 -89
  68. package/src/__tests__/system-skills-service.test.ts +0 -362
  69. package/src/__tests__/transcription-service.test.ts +0 -222
  70. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  71. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  72. package/src/__tests__/worker-job-router.test.ts +0 -722
  73. package/src/api/index.ts +0 -1
  74. package/src/api/platform.ts +0 -292
  75. package/src/api/response-renderer.ts +0 -157
  76. package/src/auth/agent-metadata-store.ts +0 -168
  77. package/src/auth/api-auth-middleware.ts +0 -69
  78. package/src/auth/api-key-provider-module.ts +0 -213
  79. package/src/auth/base-provider-module.ts +0 -201
  80. package/src/auth/bedrock/provider-module.ts +0 -110
  81. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  82. package/src/auth/chatgpt/device-code-client.ts +0 -218
  83. package/src/auth/chatgpt/index.ts +0 -1
  84. package/src/auth/claude/oauth-module.ts +0 -280
  85. package/src/auth/cli/token-service.ts +0 -249
  86. package/src/auth/external/client.ts +0 -560
  87. package/src/auth/external/device-code-client.ts +0 -235
  88. package/src/auth/mcp/config-service.ts +0 -420
  89. package/src/auth/mcp/proxy.ts +0 -1086
  90. package/src/auth/mcp/string-substitution.ts +0 -17
  91. package/src/auth/mcp/tool-cache.ts +0 -90
  92. package/src/auth/oauth/base-client.ts +0 -267
  93. package/src/auth/oauth/client.ts +0 -153
  94. package/src/auth/oauth/credentials.ts +0 -7
  95. package/src/auth/oauth/providers.ts +0 -69
  96. package/src/auth/oauth/state-store.ts +0 -150
  97. package/src/auth/oauth-templates.ts +0 -179
  98. package/src/auth/provider-catalog.ts +0 -220
  99. package/src/auth/provider-model-options.ts +0 -41
  100. package/src/auth/settings/agent-settings-store.ts +0 -565
  101. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  102. package/src/auth/settings/index.ts +0 -12
  103. package/src/auth/settings/model-preference-store.ts +0 -52
  104. package/src/auth/settings/model-selection.ts +0 -135
  105. package/src/auth/settings/resolved-settings-view.ts +0 -298
  106. package/src/auth/settings/template-utils.ts +0 -44
  107. package/src/auth/settings/token-service.ts +0 -88
  108. package/src/auth/system-env-store.ts +0 -98
  109. package/src/auth/user-agents-store.ts +0 -68
  110. package/src/channels/binding-service.ts +0 -214
  111. package/src/channels/index.ts +0 -4
  112. package/src/cli/gateway.ts +0 -1312
  113. package/src/cli/index.ts +0 -74
  114. package/src/commands/built-in-commands.ts +0 -80
  115. package/src/commands/command-dispatcher.ts +0 -94
  116. package/src/commands/command-reply-adapters.ts +0 -27
  117. package/src/config/file-loader.ts +0 -618
  118. package/src/config/index.ts +0 -588
  119. package/src/config/network-allowlist.ts +0 -71
  120. package/src/connections/chat-instance-manager.ts +0 -1284
  121. package/src/connections/chat-response-bridge.ts +0 -618
  122. package/src/connections/index.ts +0 -7
  123. package/src/connections/interaction-bridge.ts +0 -831
  124. package/src/connections/message-handler-bridge.ts +0 -440
  125. package/src/connections/platform-auth-methods.ts +0 -15
  126. package/src/connections/types.ts +0 -84
  127. package/src/gateway/connection-manager.ts +0 -291
  128. package/src/gateway/index.ts +0 -698
  129. package/src/gateway/job-router.ts +0 -201
  130. package/src/gateway-main.ts +0 -200
  131. package/src/index.ts +0 -41
  132. package/src/infrastructure/queue/index.ts +0 -12
  133. package/src/infrastructure/queue/queue-producer.ts +0 -148
  134. package/src/infrastructure/queue/redis-queue.ts +0 -361
  135. package/src/infrastructure/queue/types.ts +0 -133
  136. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  137. package/src/interactions/config-request-store.ts +0 -198
  138. package/src/interactions.ts +0 -363
  139. package/src/lobu.ts +0 -311
  140. package/src/metrics/prometheus.ts +0 -159
  141. package/src/modules/module-system.ts +0 -179
  142. package/src/orchestration/base-deployment-manager.ts +0 -900
  143. package/src/orchestration/deployment-utils.ts +0 -98
  144. package/src/orchestration/impl/docker-deployment.ts +0 -620
  145. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  146. package/src/orchestration/impl/index.ts +0 -8
  147. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  148. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  149. package/src/orchestration/impl/k8s/index.ts +0 -1
  150. package/src/orchestration/index.ts +0 -333
  151. package/src/orchestration/message-consumer.ts +0 -584
  152. package/src/orchestration/scheduled-wakeup.ts +0 -704
  153. package/src/permissions/approval-policy.ts +0 -36
  154. package/src/permissions/grant-store.ts +0 -219
  155. package/src/platform/file-handler.ts +0 -66
  156. package/src/platform/link-buttons.ts +0 -57
  157. package/src/platform/renderer-utils.ts +0 -44
  158. package/src/platform/response-renderer.ts +0 -84
  159. package/src/platform/unified-thread-consumer.ts +0 -194
  160. package/src/platform.ts +0 -318
  161. package/src/proxy/http-proxy.ts +0 -752
  162. package/src/proxy/proxy-manager.ts +0 -81
  163. package/src/proxy/secret-proxy.ts +0 -402
  164. package/src/proxy/token-refresh-job.ts +0 -143
  165. package/src/routes/internal/audio.ts +0 -141
  166. package/src/routes/internal/device-auth.ts +0 -652
  167. package/src/routes/internal/files.ts +0 -226
  168. package/src/routes/internal/history.ts +0 -69
  169. package/src/routes/internal/images.ts +0 -127
  170. package/src/routes/internal/interactions.ts +0 -84
  171. package/src/routes/internal/middleware.ts +0 -23
  172. package/src/routes/internal/schedule.ts +0 -226
  173. package/src/routes/internal/types.ts +0 -22
  174. package/src/routes/openapi-auto.ts +0 -239
  175. package/src/routes/public/agent-access.ts +0 -23
  176. package/src/routes/public/agent-config.ts +0 -675
  177. package/src/routes/public/agent-history.ts +0 -422
  178. package/src/routes/public/agent-schedules.ts +0 -296
  179. package/src/routes/public/agent.ts +0 -1086
  180. package/src/routes/public/agents.ts +0 -373
  181. package/src/routes/public/channels.ts +0 -191
  182. package/src/routes/public/cli-auth.ts +0 -896
  183. package/src/routes/public/connections.ts +0 -574
  184. package/src/routes/public/landing.ts +0 -16
  185. package/src/routes/public/oauth.ts +0 -147
  186. package/src/routes/public/settings-auth.ts +0 -104
  187. package/src/routes/public/slack.ts +0 -173
  188. package/src/routes/shared/agent-ownership.ts +0 -101
  189. package/src/routes/shared/token-verifier.ts +0 -34
  190. package/src/services/bedrock-model-catalog.ts +0 -217
  191. package/src/services/bedrock-openai-service.ts +0 -658
  192. package/src/services/core-services.ts +0 -1072
  193. package/src/services/image-generation-service.ts +0 -257
  194. package/src/services/instruction-service.ts +0 -318
  195. package/src/services/mcp-registry.ts +0 -94
  196. package/src/services/platform-helpers.ts +0 -287
  197. package/src/services/session-manager.ts +0 -262
  198. package/src/services/settings-resolver.ts +0 -74
  199. package/src/services/system-config-resolver.ts +0 -89
  200. package/src/services/system-skills-service.ts +0 -229
  201. package/src/services/transcription-service.ts +0 -684
  202. package/src/session.ts +0 -110
  203. package/src/spaces/index.ts +0 -1
  204. package/src/spaces/space-resolver.ts +0 -17
  205. package/src/stores/in-memory-agent-store.ts +0 -403
  206. package/src/stores/redis-agent-store.ts +0 -279
  207. package/src/utils/public-url.ts +0 -44
  208. package/src/utils/rate-limiter.ts +0 -94
  209. package/tsconfig.json +0 -33
  210. package/tsconfig.tsbuildinfo +0 -1
@@ -1,900 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- createLogger,
4
- ErrorCode,
5
- extractTraceId,
6
- generateWorkerToken,
7
- OrchestratorError,
8
- } from "@lobu/core";
9
- import type Redis from "ioredis";
10
- import type { MessagePayload } from "../infrastructure/queue/queue-producer";
11
- import type { ModelProviderModule } from "../modules/module-system";
12
- import type { GrantStore } from "../permissions/grant-store";
13
- import {
14
- deleteSecretMappings,
15
- generatePlaceholder,
16
- } from "../proxy/secret-proxy";
17
- import { getScheduledWakeupService } from "./scheduled-wakeup";
18
-
19
- // Re-export MessagePayload for use by deployment implementations
20
- export type { MessagePayload };
21
-
22
- const logger = createLogger("orchestrator");
23
-
24
- export interface DeploymentIdentity {
25
- conversationId: string;
26
- channelId?: string;
27
- platform?: string;
28
- userId?: string;
29
- }
30
-
31
- /**
32
- * Build a canonical conversation identity key for runtime routing.
33
- * Preferred format: platform:channelId:conversationId
34
- */
35
- export function buildCanonicalConversationKey(
36
- identity: DeploymentIdentity
37
- ): string {
38
- const { conversationId, channelId, platform } = identity;
39
- if (platform && channelId) {
40
- return `${platform}:${channelId}:${conversationId}`;
41
- }
42
- if (channelId) {
43
- return `${channelId}:${conversationId}`;
44
- }
45
- return conversationId;
46
- }
47
-
48
- function sanitizeNameHint(value: string | undefined, fallback: string): string {
49
- const sanitized = (value || "").replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
50
- return (sanitized.slice(0, 8) || fallback).toLowerCase();
51
- }
52
-
53
- /**
54
- * Generate a consistent worker runtime ID from canonical conversation identity.
55
- * Overload preserved for compatibility with older callers.
56
- * K8s names must be lowercase alphanumeric with hyphens only.
57
- */
58
- export function generateDeploymentName(
59
- userId: string,
60
- conversationId: string
61
- ): string;
62
- export function generateDeploymentName(identity: DeploymentIdentity): string;
63
- export function generateDeploymentName(
64
- arg1: string | DeploymentIdentity,
65
- arg2?: string
66
- ): string {
67
- if (typeof arg1 === "string") {
68
- const userId = arg1;
69
- const conversationId = arg2 || "";
70
- const shortHint = sanitizeNameHint(userId, "user");
71
- const hash = createHash("sha256")
72
- .update(`${userId}:${conversationId}`)
73
- .digest("hex")
74
- .slice(0, 12);
75
- return `lobu-worker-${shortHint}-${hash}`;
76
- }
77
-
78
- const identity = arg1;
79
- const canonicalKey = buildCanonicalConversationKey(identity);
80
- const hint = sanitizeNameHint(identity.platform || identity.userId, "ctx");
81
- const hash = createHash("sha256")
82
- .update(canonicalKey)
83
- .digest("hex")
84
- .slice(0, 12);
85
- return `lobu-worker-${hint}-${hash}`;
86
- }
87
-
88
- // Type for module environment variable builder function
89
- export type ModuleEnvVarsBuilder = (
90
- agentId: string,
91
- envVars: Record<string, string>
92
- ) => Promise<Record<string, string>>;
93
-
94
- // Orchestrator configuration
95
- export interface OrchestratorConfig {
96
- deploymentMode?: "embedded" | "docker" | "kubernetes";
97
- queues: {
98
- connectionString: string;
99
- retryLimit: number;
100
- retryDelay: number;
101
- expireInSeconds: number;
102
- };
103
- worker: {
104
- image: {
105
- repository: string;
106
- tag: string;
107
- digest?: string;
108
- pullPolicy: string;
109
- };
110
- serviceAccountName?: string;
111
- imagePullSecrets?: string[];
112
- runtimeClassName?: string; // Optional - if not set or unavailable, uses default container runtime
113
- startupTimeoutSeconds?: number;
114
- resources: {
115
- requests: { cpu: string; memory: string };
116
- limits: { cpu: string; memory: string };
117
- };
118
- idleCleanupMinutes: number;
119
- maxDeployments: number;
120
- env?: Record<string, string | number | boolean>;
121
- persistence?: {
122
- size?: string;
123
- storageClass?: string;
124
- };
125
- };
126
- kubernetes: {
127
- namespace: string;
128
- };
129
- cleanup: {
130
- initialDelayMs: number;
131
- intervalMs: number;
132
- veryOldDays: number;
133
- };
134
- }
135
-
136
- export interface DeploymentInfo {
137
- deploymentName: string;
138
- lastActivity: Date;
139
- minutesIdle: number;
140
- daysSinceActivity: number;
141
- replicas: number;
142
- isIdle: boolean;
143
- isVeryOld: boolean;
144
- }
145
-
146
- /** Check if an env var name looks like a secret (API key / token / secret / password). */
147
- function isSecretEnvVar(
148
- name: string,
149
- providerModules: ModelProviderModule[]
150
- ): boolean {
151
- for (const provider of providerModules) {
152
- if (provider.getSecretEnvVarNames().includes(name)) return true;
153
- }
154
- const upper = name.toUpperCase();
155
- return (
156
- upper.includes("_KEY") ||
157
- upper.includes("_TOKEN") ||
158
- upper.includes("_SECRET") ||
159
- upper.includes("_PASSWORD")
160
- );
161
- }
162
-
163
- export abstract class BaseDeploymentManager {
164
- protected config: OrchestratorConfig;
165
- protected moduleEnvVarsBuilder?: ModuleEnvVarsBuilder;
166
- protected providerModules: ModelProviderModule[];
167
- protected providerCatalogService?: import("../auth/provider-catalog").ProviderCatalogService;
168
- protected redisClient?: Redis;
169
- protected grantStore?: GrantStore;
170
-
171
- constructor(
172
- config: OrchestratorConfig,
173
- moduleEnvVarsBuilder?: ModuleEnvVarsBuilder,
174
- providerModules: ModelProviderModule[] = []
175
- ) {
176
- this.config = config;
177
- this.moduleEnvVarsBuilder = moduleEnvVarsBuilder;
178
- this.providerModules = providerModules;
179
- }
180
-
181
- /**
182
- * Inject Redis client for secret placeholder generation.
183
- * Called after core services are initialized.
184
- */
185
- setRedisClient(redis: Redis): void {
186
- this.redisClient = redis;
187
- }
188
-
189
- /**
190
- * Refresh provider modules after module registry initialization.
191
- */
192
- setProviderModules(providerModules: ModelProviderModule[]): void {
193
- this.providerModules = providerModules;
194
- }
195
-
196
- setProviderCatalogService(
197
- service: import("../auth/provider-catalog").ProviderCatalogService
198
- ): void {
199
- this.providerCatalogService = service;
200
- }
201
-
202
- /**
203
- * Inject grant store for auto-adding domain grants at deployment time.
204
- */
205
- setGrantStore(store: GrantStore): void {
206
- this.grantStore = store;
207
- }
208
-
209
- /**
210
- * Get the dispatcher URL for the worker gateway service (port 8080)
211
- */
212
- protected getDispatcherUrl(): string {
213
- return `http://${this.getDispatcherHost()}:8080`;
214
- }
215
-
216
- // Abstract methods that must be implemented by concrete classes
217
- abstract listDeployments(): Promise<DeploymentInfo[]>;
218
- abstract createDeployment(
219
- deploymentName: string,
220
- username: string,
221
- userId: string,
222
- messageData?: MessagePayload
223
- ): Promise<void>;
224
- abstract scaleDeployment(
225
- deploymentName: string,
226
- replicas: number
227
- ): Promise<void>;
228
- abstract deleteDeployment(deploymentName: string): Promise<void>;
229
- abstract updateDeploymentActivity(deploymentName: string): Promise<void>;
230
- abstract validateWorkerImage(): Promise<void>;
231
-
232
- /**
233
- * Get the dispatcher service host (without port)
234
- * Implementations return the appropriate host for their deployment mode
235
- */
236
- protected abstract getDispatcherHost(): string;
237
-
238
- /**
239
- * Resolve worker image reference.
240
- * If digest is configured, prefer immutable digest reference (repo@sha256:...).
241
- */
242
- protected getWorkerImageReference(): string {
243
- const { repository, tag, digest } = this.config.worker.image;
244
- const normalizedDigest = digest?.trim();
245
-
246
- if (normalizedDigest) {
247
- const digestWithAlgo = normalizedDigest.startsWith("sha256:")
248
- ? normalizedDigest
249
- : `sha256:${normalizedDigest}`;
250
- return `${repository}@${digestWithAlgo}`;
251
- }
252
-
253
- return `${repository}:${tag}`;
254
- }
255
-
256
- /**
257
- * Create worker deployment for handling messages.
258
- * @param existingDeployments - Optional pre-fetched deployment list to avoid redundant API calls
259
- */
260
- async createWorkerDeployment(
261
- userId: string,
262
- conversationId: string,
263
- messageData?: MessagePayload,
264
- existingDeployments?: DeploymentInfo[]
265
- ): Promise<void> {
266
- const deploymentIdentity: DeploymentIdentity = {
267
- userId,
268
- conversationId,
269
- channelId: messageData?.channelId,
270
- platform: messageData?.platform,
271
- };
272
- const deploymentName = generateDeploymentName(deploymentIdentity);
273
- const canonicalConversationKey =
274
- buildCanonicalConversationKey(deploymentIdentity);
275
-
276
- logger.info(
277
- `Worker deployment - conversationId: ${conversationId}, canonicalKey: ${canonicalConversationKey}, deploymentName: ${deploymentName}`
278
- );
279
-
280
- try {
281
- // Use pre-fetched list or fetch fresh
282
- const deployments = existingDeployments ?? (await this.listDeployments());
283
- const existingDeployment = deployments.find(
284
- (d) => d.deploymentName === deploymentName
285
- );
286
-
287
- if (existingDeployment) {
288
- // Scale up the existing deployment. Provider config is now delivered
289
- // dynamically via session context, so no need to recreate.
290
- await this.scaleDeployment(deploymentName, 1);
291
- return;
292
- }
293
-
294
- // Check if we would exceed max deployments limit
295
- const maxDeployments = this.config.worker.maxDeployments;
296
- if (maxDeployments > 0 && deployments.length >= maxDeployments) {
297
- logger.warn(
298
- `⚠️ Maximum deployments limit reached (${deployments.length}/${maxDeployments}). Running cleanup before creating new deployment.`
299
- );
300
- await this.reconcileDeployments();
301
-
302
- // Check again after cleanup
303
- const deploymentsAfterCleanup = await this.listDeployments();
304
- if (deploymentsAfterCleanup.length >= maxDeployments) {
305
- throw new OrchestratorError(
306
- ErrorCode.DEPLOYMENT_CREATE_FAILED,
307
- `Cannot create new deployment: Maximum deployments limit (${maxDeployments}) reached. Current active deployments: ${deploymentsAfterCleanup.length}`,
308
- {
309
- maxDeployments,
310
- currentCount: deploymentsAfterCleanup.length,
311
- },
312
- true
313
- );
314
- }
315
- }
316
-
317
- await this.createDeployment(deploymentName, userId, userId, messageData);
318
- } catch (error) {
319
- throw new OrchestratorError(
320
- ErrorCode.DEPLOYMENT_CREATE_FAILED,
321
- `Failed to create worker deployment: ${error instanceof Error ? error.message : String(error)}`,
322
- { userId, conversationId, error },
323
- true
324
- );
325
- }
326
- }
327
-
328
- /**
329
- * Validate that messageData has all required fields for deployment.
330
- */
331
- private validateMessageData(
332
- deploymentName: string,
333
- messageData?: MessagePayload
334
- ): MessagePayload {
335
- if (!messageData) {
336
- throw new OrchestratorError(
337
- ErrorCode.DEPLOYMENT_CREATE_FAILED,
338
- "Message data is required for worker deployment",
339
- { deploymentName },
340
- true
341
- );
342
- }
343
-
344
- const { conversationId, channelId } = messageData;
345
- if (!conversationId || !channelId) {
346
- throw new OrchestratorError(
347
- ErrorCode.DEPLOYMENT_CREATE_FAILED,
348
- "conversationId and channelId are required in message data",
349
- {
350
- deploymentName,
351
- hasConversationId: !!conversationId,
352
- hasChannelId: !!channelId,
353
- },
354
- true
355
- );
356
- }
357
-
358
- return messageData;
359
- }
360
-
361
- /**
362
- * Auto-add Nix cache domains as grants and persist MCP configs for the deployment.
363
- */
364
- private async storeDeploymentConfigs(
365
- deploymentName: string,
366
- messageData: MessagePayload
367
- ): Promise<void> {
368
- const agentId = messageData.agentId;
369
-
370
- // Sync networkConfig.allowedDomains to grant store
371
- if (
372
- this.grantStore &&
373
- agentId &&
374
- messageData.networkConfig?.allowedDomains?.length
375
- ) {
376
- for (const domain of messageData.networkConfig.allowedDomains) {
377
- await this.grantStore.grant(agentId, domain, null);
378
- }
379
- logger.info(
380
- `Synced network config domains as grants for ${deploymentName}: ${messageData.networkConfig.allowedDomains.join(", ")}`
381
- );
382
- }
383
-
384
- // Auto-add Nix cache domains as permanent grants when Nix packages are configured
385
- if (
386
- this.grantStore &&
387
- agentId &&
388
- (messageData.nixConfig?.packages?.length ||
389
- messageData.nixConfig?.flakeUrl)
390
- ) {
391
- const NIX_DOMAINS = [
392
- "cache.nixos.org",
393
- "channels.nixos.org",
394
- "releases.nixos.org",
395
- ];
396
- for (const domain of NIX_DOMAINS) {
397
- await this.grantStore.grant(agentId, domain, null);
398
- }
399
- logger.info(
400
- `Added Nix cache domains as grants for ${deploymentName}: ${NIX_DOMAINS.join(", ")}`
401
- );
402
- }
403
- }
404
-
405
- /**
406
- * Sync networkConfig.allowedDomains to the grant store for a running worker.
407
- * Called on every message to pick up domains added via configuration APIs.
408
- */
409
- async syncNetworkConfigGrants(messageData: MessagePayload): Promise<void> {
410
- const agentId = messageData.agentId;
411
- if (!this.grantStore || !agentId) return;
412
-
413
- if (messageData.networkConfig?.allowedDomains?.length) {
414
- for (const domain of messageData.networkConfig.allowedDomains) {
415
- await this.grantStore.grant(agentId, domain, null);
416
- }
417
- }
418
- }
419
-
420
- /**
421
- * Build proxy URL with deployment identification via Basic auth.
422
- */
423
- private buildProxyUrl(
424
- deploymentName: string,
425
- workerToken: string,
426
- dispatcherHost: string
427
- ): string {
428
- const parsedProxyPort = Number.parseInt(
429
- process.env.WORKER_PROXY_PORT || "8118",
430
- 10
431
- );
432
- const proxyPort = Number.isFinite(parsedProxyPort) ? parsedProxyPort : 8118;
433
- return `http://${deploymentName}:${workerToken}@${dispatcherHost}:${proxyPort}`;
434
- }
435
-
436
- /**
437
- * Assemble the base environment variables map for a worker deployment.
438
- */
439
- private assembleBaseEnv(
440
- username: string,
441
- userId: string,
442
- deploymentName: string,
443
- workerToken: string,
444
- messageData: MessagePayload,
445
- traceId: string | undefined,
446
- proxyUrl: string,
447
- dispatcherHost: string
448
- ): Record<string, string> {
449
- const { conversationId, channelId, platformMetadata } = messageData;
450
-
451
- const envVars: Record<string, string> = {
452
- USER_ID: userId,
453
- USERNAME: username,
454
- DEPLOYMENT_NAME: deploymentName,
455
- CHANNEL_ID: channelId,
456
- ORIGINAL_MESSAGE_TS:
457
- platformMetadata?.originalMessageTs || messageData.messageId || "",
458
- LOG_LEVEL: "info",
459
- WORKSPACE_DIR: "/workspace",
460
- CONVERSATION_ID: conversationId,
461
- WORKER_TOKEN: workerToken,
462
- DISPATCHER_URL: this.getDispatcherUrl(),
463
- NODE_ENV: process.env.NODE_ENV || "production",
464
- DEBUG: "1",
465
- HTTP_PROXY: proxyUrl,
466
- HTTPS_PROXY: proxyUrl,
467
- NO_PROXY: `${dispatcherHost},gateway,redis,localhost,127.0.0.1`,
468
- // Route temporary files and cache to persistent workspace storage.
469
- TMPDIR: "/workspace/.tmp",
470
- TMP: "/workspace/.tmp",
471
- TEMP: "/workspace/.tmp",
472
- XDG_CACHE_HOME: "/workspace/.cache",
473
- };
474
-
475
- if (platformMetadata?.botResponseTs) {
476
- envVars.BOT_RESPONSE_TS = platformMetadata.botResponseTs;
477
- }
478
-
479
- if (traceId) {
480
- envVars.TRACE_ID = traceId;
481
- }
482
-
483
- // Add OTLP endpoint for distributed tracing
484
- const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
485
- if (otlpEndpoint) {
486
- envVars.OTEL_EXPORTER_OTLP_ENDPOINT = otlpEndpoint;
487
- try {
488
- const otlpUrl = new URL(otlpEndpoint);
489
- envVars.NO_PROXY = `${envVars.NO_PROXY},${otlpUrl.hostname}`;
490
- } catch {
491
- envVars.NO_PROXY = `${envVars.NO_PROXY},tempo`;
492
- }
493
- }
494
-
495
- // Forward WORKER_ENV_* vars to workers with prefix stripped
496
- const WORKER_ENV_PREFIX = "WORKER_ENV_";
497
- for (const key of Object.keys(process.env)) {
498
- if (key.startsWith(WORKER_ENV_PREFIX)) {
499
- const stripped = key.slice(WORKER_ENV_PREFIX.length);
500
- if (stripped) {
501
- envVars[stripped] = process.env[key]!;
502
- }
503
- }
504
- }
505
-
506
- // Nix config
507
- if (messageData.nixConfig) {
508
- const { flakeUrl, packages } = messageData.nixConfig;
509
- if (flakeUrl) envVars.NIX_FLAKE_URL = flakeUrl;
510
- if (packages && packages.length > 0)
511
- envVars.NIX_PACKAGES = packages.join(",");
512
- logger.debug(
513
- `Nix config for ${deploymentName}: flakeUrl=${flakeUrl || "none"}, packages=${packages?.length || 0}`
514
- );
515
- }
516
-
517
- return envVars;
518
- }
519
-
520
- /**
521
- * Replace secret env var values with opaque placeholders before passing to workers.
522
- *
523
- * Provider credential env vars are set to `"lobu-proxy"` — the proxy resolves
524
- * the real credential at request time using agentId from the URL path
525
- * (`/a/{agentId}`) and the provider slug.
526
- *
527
- * Non-provider secrets use UUID placeholders stored in Redis.
528
- */
529
- private async injectSecretPlaceholders(
530
- envVars: Record<string, string>,
531
- agentId: string,
532
- deploymentName: string
533
- ): Promise<Record<string, string>> {
534
- if (!this.redisClient) return envVars;
535
-
536
- // Collect credential env var names from all providers
537
- const providerCredentialVars = new Set<string>();
538
- for (const provider of this.providerModules) {
539
- providerCredentialVars.add(provider.getCredentialEnvVarName());
540
- }
541
-
542
- let hasSecrets = false;
543
- for (const [key, value] of Object.entries(envVars)) {
544
- if (!value || !isSecretEnvVar(key, this.providerModules)) continue;
545
- if (key === "WORKER_TOKEN") continue;
546
-
547
- if (providerCredentialVars.has(key)) {
548
- // Provider credentials use a proxy placeholder. The worker never
549
- // sees real credentials. The proxy resolves the real credential
550
- // using agentId from the URL path (/a/{agentId}) and the provider
551
- // slug, then overrides the Authorization header before forwarding.
552
- const ownerProvider = this.providerModules.find(
553
- (p) => p.getCredentialEnvVarName() === key
554
- );
555
- if (ownerProvider?.buildCredentialPlaceholder) {
556
- envVars[key] =
557
- await ownerProvider.buildCredentialPlaceholder(agentId);
558
- } else {
559
- envVars[key] = "lobu-proxy";
560
- }
561
- hasSecrets = true;
562
- } else {
563
- // Use UUID placeholder for non-provider secrets (legacy path)
564
- try {
565
- const placeholder = await generatePlaceholder(
566
- this.redisClient,
567
- agentId,
568
- key,
569
- value,
570
- deploymentName
571
- );
572
- envVars[key] = placeholder;
573
- hasSecrets = true;
574
- } catch (error) {
575
- logger.warn(`Failed to generate placeholder for ${key}:`, error);
576
- }
577
- }
578
- }
579
-
580
- if (hasSecrets) {
581
- const proxyUrl = `${this.getDispatcherUrl()}/api/proxy`;
582
- for (const provider of this.providerModules) {
583
- Object.assign(
584
- envVars,
585
- provider.getProxyBaseUrlMappings(proxyUrl, agentId)
586
- );
587
- }
588
- logger.info(
589
- `🔐 Generated secret placeholders for ${deploymentName}, routing through proxy`
590
- );
591
- }
592
-
593
- return envVars;
594
- }
595
-
596
- /**
597
- * Generate environment variables common to all deployment types.
598
- * Orchestrates the focused helpers above.
599
- */
600
- protected async generateEnvironmentVariables(
601
- username: string,
602
- userId: string,
603
- deploymentName: string,
604
- messageData?: MessagePayload,
605
- includeSecrets: boolean = true
606
- ): Promise<Record<string, string>> {
607
- const validated = this.validateMessageData(deploymentName, messageData);
608
- const { conversationId, channelId, platformMetadata, agentId, platform } =
609
- validated;
610
- const teamId = validated.teamId || platformMetadata?.teamId;
611
- const traceId = extractTraceId(validated);
612
-
613
- const workerToken = generateWorkerToken(
614
- userId,
615
- conversationId,
616
- deploymentName,
617
- {
618
- channelId,
619
- teamId,
620
- platform,
621
- agentId,
622
- connectionId:
623
- typeof platformMetadata?.connectionId === "string"
624
- ? platformMetadata.connectionId
625
- : undefined,
626
- traceId,
627
- }
628
- );
629
-
630
- const dispatcherHost = this.getDispatcherHost();
631
- await this.storeDeploymentConfigs(deploymentName, validated);
632
-
633
- const proxyUrl = this.buildProxyUrl(
634
- deploymentName,
635
- workerToken,
636
- dispatcherHost
637
- );
638
-
639
- let envVars = this.assembleBaseEnv(
640
- username,
641
- userId,
642
- deploymentName,
643
- workerToken,
644
- validated,
645
- traceId,
646
- proxyUrl,
647
- dispatcherHost
648
- );
649
-
650
- // Include secrets from process.env for Docker deployments
651
- if (includeSecrets && this.moduleEnvVarsBuilder) {
652
- try {
653
- envVars = await this.moduleEnvVarsBuilder(agentId, envVars);
654
- } catch (error) {
655
- logger.warn("Failed to build module environment variables:", error);
656
- }
657
- }
658
-
659
- // Add worker environment variables from configuration
660
- if (this.config.worker.env) {
661
- for (const [key, value] of Object.entries(this.config.worker.env)) {
662
- envVars[key] = String(value);
663
- }
664
- }
665
-
666
- // Resolve per-agent installed providers (catalog-only when active, no global fallback)
667
- const effectiveProviders = this.providerCatalogService
668
- ? await this.providerCatalogService.getInstalledModules(agentId)
669
- : this.providerModules;
670
-
671
- for (const provider of effectiveProviders) {
672
- envVars = provider.injectSystemKeyFallback(envVars);
673
- }
674
-
675
- envVars = await this.injectSecretPlaceholders(
676
- envVars,
677
- agentId,
678
- deploymentName
679
- );
680
-
681
- // Inject provider metadata into agentOptions so the worker can configure
682
- // the SDK generically without hardcoded provider checks.
683
- // Determine primary provider from the model in agentOptions.
684
- const agentModel = validated.agentOptions?.model as string | undefined;
685
- let primaryProvider: ModelProviderModule | undefined;
686
-
687
- if (
688
- agentModel &&
689
- effectiveProviders.length > 0 &&
690
- this.providerCatalogService
691
- ) {
692
- primaryProvider = await this.providerCatalogService.findProviderForModel(
693
- agentModel,
694
- effectiveProviders
695
- );
696
- }
697
-
698
- // When no explicit model is set (auto mode), detect the primary provider
699
- // from installed providers order (first with credentials = primary).
700
- if (!primaryProvider && effectiveProviders.length > 0) {
701
- for (const candidate of effectiveProviders) {
702
- if (
703
- candidate.hasSystemKey() ||
704
- (await candidate.hasCredentials(agentId))
705
- ) {
706
- primaryProvider = candidate;
707
- break;
708
- }
709
- }
710
- }
711
-
712
- if (primaryProvider) {
713
- logger.info(
714
- {
715
- agentId,
716
- primaryProviderId: primaryProvider.providerId,
717
- slug: primaryProvider.getUpstreamConfig?.()?.slug,
718
- },
719
- "Selected primary provider"
720
- );
721
-
722
- const proxyBaseUrl = `${this.getDispatcherUrl()}/api/proxy`;
723
- const mappings = primaryProvider.getProxyBaseUrlMappings(
724
- proxyBaseUrl,
725
- agentId
726
- );
727
- const providerBaseUrl = Object.values(mappings)[0];
728
- if (providerBaseUrl) {
729
- validated.agentOptions = {
730
- ...validated.agentOptions,
731
- providerBaseUrl,
732
- };
733
- }
734
-
735
- // CREDENTIAL_ENV_VAR_NAME and AGENT_DEFAULT_PROVIDER are now
736
- // delivered dynamically via session context endpoint. No longer
737
- // set as static container env vars.
738
- }
739
-
740
- // Build full provider base URL mappings for all installed providers
741
- const proxyBaseUrl = `${this.getDispatcherUrl()}/api/proxy`;
742
- const providerBaseUrlMappings: Record<string, string> = {};
743
- for (const provider of effectiveProviders) {
744
- const mappings = provider.getProxyBaseUrlMappings(proxyBaseUrl, agentId);
745
- Object.assign(providerBaseUrlMappings, mappings);
746
- }
747
- if (Object.keys(providerBaseUrlMappings).length > 0) {
748
- validated.agentOptions = {
749
- ...validated.agentOptions,
750
- providerBaseUrlMappings,
751
- };
752
- }
753
-
754
- // CLI_BACKENDS is now delivered dynamically via session context.
755
- // Still need to auto-add npm registry domains for npx at deploy time.
756
- const hasCliBackendProviders = effectiveProviders.some((p) =>
757
- p.getCliBackendConfig?.()
758
- );
759
- if (hasCliBackendProviders && this.grantStore && agentId) {
760
- const NPM_DOMAINS = ["registry.npmjs.org", "registry.npmmirror.com"];
761
- for (const domain of NPM_DOMAINS) {
762
- await this.grantStore.grant(agentId, domain, null);
763
- }
764
- logger.info(
765
- `Added npm registry domains as grants for ${deploymentName}: ${NPM_DOMAINS.join(", ")}`
766
- );
767
- }
768
-
769
- return envVars;
770
- }
771
-
772
- /**
773
- * Delete a worker deployment and associated resources
774
- */
775
- async deleteWorkerDeployment(deploymentName: string): Promise<void> {
776
- try {
777
- // Clean up secret placeholder mappings
778
- if (this.redisClient) {
779
- await deleteSecretMappings(this.redisClient, deploymentName);
780
- }
781
-
782
- // Clean up any scheduled wakeups for this deployment
783
- const scheduledWakeupService = getScheduledWakeupService();
784
- if (scheduledWakeupService) {
785
- await scheduledWakeupService.cleanupForDeployment(deploymentName);
786
- }
787
-
788
- await this.deleteDeployment(deploymentName);
789
- } catch (error) {
790
- throw new OrchestratorError(
791
- ErrorCode.DEPLOYMENT_DELETE_FAILED,
792
- `Failed to delete deployment for ${deploymentName}: ${error instanceof Error ? error.message : String(error)}`,
793
- { deploymentName, error },
794
- true
795
- );
796
- }
797
- }
798
-
799
- /**
800
- * Reconcile deployments: unified method for cleanup and resource management
801
- * This method uses the abstract methods to work with any deployment backend
802
- */
803
- async reconcileDeployments(): Promise<void> {
804
- try {
805
- const maxDeployments = this.config.worker.maxDeployments;
806
-
807
- logger.debug("Running deployment cleanup...");
808
-
809
- // Get all worker deployments from the backend
810
- const activeDeployments = await this.listDeployments();
811
-
812
- if (activeDeployments.length === 0) {
813
- return;
814
- }
815
-
816
- // Sort deployments by last activity (oldest first)
817
- const sortedDeployments = [...activeDeployments].sort(
818
- (a, b) => a.lastActivity.getTime() - b.lastActivity.getTime()
819
- );
820
-
821
- let processedCount = 0;
822
- const BATCH_SIZE = 10; // Process up to 10 deletions in parallel
823
-
824
- // Collect actions to perform
825
- const toDelete: string[] = [];
826
- const toScaleDown: string[] = [];
827
-
828
- for (const analysis of sortedDeployments) {
829
- const { deploymentName, replicas, isIdle, isVeryOld } = analysis;
830
-
831
- if (isVeryOld) {
832
- toDelete.push(deploymentName);
833
- } else if (isIdle && replicas > 0) {
834
- toScaleDown.push(deploymentName);
835
- }
836
- }
837
-
838
- // Check if we exceed max deployments
839
- const remainingDeployments = sortedDeployments.filter(
840
- (d) => !d.isVeryOld
841
- );
842
- if (remainingDeployments.length > maxDeployments) {
843
- const excessCount = remainingDeployments.length - maxDeployments;
844
- const deploymentsToDelete = remainingDeployments.slice(0, excessCount);
845
- for (const { deploymentName } of deploymentsToDelete) {
846
- if (!toDelete.includes(deploymentName)) {
847
- toDelete.push(deploymentName);
848
- }
849
- }
850
- }
851
-
852
- // Process deletions in parallel batches
853
- for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
854
- const batch = toDelete.slice(i, i + BATCH_SIZE);
855
- const results = await Promise.allSettled(
856
- batch.map((name) => this.deleteWorkerDeployment(name))
857
- );
858
- for (let j = 0; j < results.length; j++) {
859
- if (results[j]?.status === "fulfilled") {
860
- processedCount++;
861
- } else {
862
- logger.error(
863
- `❌ Failed to delete deployment ${batch[j]}:`,
864
- (results[j] as PromiseRejectedResult).reason
865
- );
866
- }
867
- }
868
- }
869
-
870
- // Process scale-downs in parallel batches
871
- for (let i = 0; i < toScaleDown.length; i += BATCH_SIZE) {
872
- const batch = toScaleDown.slice(i, i + BATCH_SIZE);
873
- const results = await Promise.allSettled(
874
- batch.map((name) => this.scaleDeployment(name, 0))
875
- );
876
- for (let j = 0; j < results.length; j++) {
877
- if (results[j]?.status === "fulfilled") {
878
- processedCount++;
879
- } else {
880
- logger.error(
881
- `❌ Failed to scale down deployment ${batch[j]}:`,
882
- (results[j] as PromiseRejectedResult).reason
883
- );
884
- }
885
- }
886
- }
887
-
888
- if (processedCount > 0) {
889
- logger.info(
890
- `✅ Cleanup completed: processed ${processedCount} deployment(s)`
891
- );
892
- }
893
- } catch (error) {
894
- logger.error(
895
- "Error during deployment reconciliation:",
896
- error instanceof Error ? error.message : String(error)
897
- );
898
- }
899
- }
900
- }