@lobu/gateway 3.0.8 → 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 (219) hide show
  1. package/dist/api/platform.d.ts.map +1 -1
  2. package/dist/api/platform.js +8 -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/cli/index.js +2 -2
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/connections/chat-instance-manager.d.ts.map +1 -1
  14. package/dist/connections/chat-instance-manager.js +2 -1
  15. package/dist/connections/chat-instance-manager.js.map +1 -1
  16. package/dist/connections/interaction-bridge.d.ts +9 -2
  17. package/dist/connections/interaction-bridge.d.ts.map +1 -1
  18. package/dist/connections/interaction-bridge.js +132 -230
  19. package/dist/connections/interaction-bridge.js.map +1 -1
  20. package/dist/connections/message-handler-bridge.d.ts.map +1 -1
  21. package/dist/connections/message-handler-bridge.js +44 -26
  22. package/dist/connections/message-handler-bridge.js.map +1 -1
  23. package/dist/interactions.d.ts +9 -43
  24. package/dist/interactions.d.ts.map +1 -1
  25. package/dist/interactions.js +10 -52
  26. package/dist/interactions.js.map +1 -1
  27. package/dist/orchestration/base-deployment-manager.js +7 -7
  28. package/dist/orchestration/base-deployment-manager.js.map +1 -1
  29. package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
  30. package/dist/platform/unified-thread-consumer.js +38 -34
  31. package/dist/platform/unified-thread-consumer.js.map +1 -1
  32. package/dist/routes/public/agent.d.ts +4 -0
  33. package/dist/routes/public/agent.d.ts.map +1 -1
  34. package/dist/routes/public/agent.js +21 -0
  35. package/dist/routes/public/agent.js.map +1 -1
  36. package/dist/services/core-services.d.ts.map +1 -1
  37. package/dist/services/core-services.js +4 -0
  38. package/dist/services/core-services.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/agent-config-routes.test.ts +0 -254
  41. package/src/__tests__/agent-history-routes.test.ts +0 -72
  42. package/src/__tests__/agent-routes.test.ts +0 -68
  43. package/src/__tests__/agent-schedules-routes.test.ts +0 -59
  44. package/src/__tests__/agent-settings-store.test.ts +0 -323
  45. package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
  46. package/src/__tests__/bedrock-openai-service.test.ts +0 -157
  47. package/src/__tests__/bedrock-provider-module.test.ts +0 -56
  48. package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
  49. package/src/__tests__/chat-response-bridge.test.ts +0 -131
  50. package/src/__tests__/config-memory-plugins.test.ts +0 -92
  51. package/src/__tests__/config-request-store.test.ts +0 -127
  52. package/src/__tests__/connection-routes.test.ts +0 -144
  53. package/src/__tests__/core-services-store-selection.test.ts +0 -92
  54. package/src/__tests__/docker-deployment.test.ts +0 -1211
  55. package/src/__tests__/embedded-deployment.test.ts +0 -342
  56. package/src/__tests__/grant-store.test.ts +0 -148
  57. package/src/__tests__/http-proxy.test.ts +0 -281
  58. package/src/__tests__/instruction-service.test.ts +0 -37
  59. package/src/__tests__/link-buttons.test.ts +0 -112
  60. package/src/__tests__/lobu.test.ts +0 -32
  61. package/src/__tests__/mcp-config-service.test.ts +0 -347
  62. package/src/__tests__/mcp-proxy.test.ts +0 -694
  63. package/src/__tests__/message-handler-bridge.test.ts +0 -17
  64. package/src/__tests__/model-selection.test.ts +0 -172
  65. package/src/__tests__/oauth-templates.test.ts +0 -39
  66. package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
  67. package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
  68. package/src/__tests__/provider-inheritance.test.ts +0 -212
  69. package/src/__tests__/routes/cli-auth.test.ts +0 -337
  70. package/src/__tests__/routes/interactions.test.ts +0 -121
  71. package/src/__tests__/secret-proxy.test.ts +0 -85
  72. package/src/__tests__/session-manager.test.ts +0 -572
  73. package/src/__tests__/setup.ts +0 -133
  74. package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
  75. package/src/__tests__/slack-routes.test.ts +0 -161
  76. package/src/__tests__/system-config-resolver.test.ts +0 -75
  77. package/src/__tests__/system-message-limiter.test.ts +0 -89
  78. package/src/__tests__/system-skills-service.test.ts +0 -362
  79. package/src/__tests__/transcription-service.test.ts +0 -222
  80. package/src/__tests__/utils/rate-limiter.test.ts +0 -102
  81. package/src/__tests__/worker-connection-manager.test.ts +0 -497
  82. package/src/__tests__/worker-job-router.test.ts +0 -722
  83. package/src/api/index.ts +0 -1
  84. package/src/api/platform.ts +0 -292
  85. package/src/api/response-renderer.ts +0 -157
  86. package/src/auth/agent-metadata-store.ts +0 -168
  87. package/src/auth/api-auth-middleware.ts +0 -69
  88. package/src/auth/api-key-provider-module.ts +0 -213
  89. package/src/auth/base-provider-module.ts +0 -201
  90. package/src/auth/bedrock/provider-module.ts +0 -110
  91. package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
  92. package/src/auth/chatgpt/device-code-client.ts +0 -218
  93. package/src/auth/chatgpt/index.ts +0 -1
  94. package/src/auth/claude/oauth-module.ts +0 -280
  95. package/src/auth/cli/token-service.ts +0 -249
  96. package/src/auth/external/client.ts +0 -560
  97. package/src/auth/external/device-code-client.ts +0 -235
  98. package/src/auth/mcp/config-service.ts +0 -420
  99. package/src/auth/mcp/proxy.ts +0 -1086
  100. package/src/auth/mcp/string-substitution.ts +0 -17
  101. package/src/auth/mcp/tool-cache.ts +0 -90
  102. package/src/auth/oauth/base-client.ts +0 -267
  103. package/src/auth/oauth/client.ts +0 -153
  104. package/src/auth/oauth/credentials.ts +0 -7
  105. package/src/auth/oauth/providers.ts +0 -69
  106. package/src/auth/oauth/state-store.ts +0 -150
  107. package/src/auth/oauth-templates.ts +0 -179
  108. package/src/auth/provider-catalog.ts +0 -220
  109. package/src/auth/provider-model-options.ts +0 -41
  110. package/src/auth/settings/agent-settings-store.ts +0 -565
  111. package/src/auth/settings/auth-profiles-manager.ts +0 -216
  112. package/src/auth/settings/index.ts +0 -12
  113. package/src/auth/settings/model-preference-store.ts +0 -52
  114. package/src/auth/settings/model-selection.ts +0 -135
  115. package/src/auth/settings/resolved-settings-view.ts +0 -298
  116. package/src/auth/settings/template-utils.ts +0 -44
  117. package/src/auth/settings/token-service.ts +0 -88
  118. package/src/auth/system-env-store.ts +0 -98
  119. package/src/auth/user-agents-store.ts +0 -68
  120. package/src/channels/binding-service.ts +0 -214
  121. package/src/channels/index.ts +0 -4
  122. package/src/cli/gateway.ts +0 -1312
  123. package/src/cli/index.ts +0 -74
  124. package/src/commands/built-in-commands.ts +0 -80
  125. package/src/commands/command-dispatcher.ts +0 -94
  126. package/src/commands/command-reply-adapters.ts +0 -27
  127. package/src/config/file-loader.ts +0 -618
  128. package/src/config/index.ts +0 -588
  129. package/src/config/network-allowlist.ts +0 -71
  130. package/src/connections/chat-instance-manager.ts +0 -1284
  131. package/src/connections/chat-response-bridge.ts +0 -618
  132. package/src/connections/index.ts +0 -7
  133. package/src/connections/interaction-bridge.ts +0 -831
  134. package/src/connections/message-handler-bridge.ts +0 -415
  135. package/src/connections/platform-auth-methods.ts +0 -15
  136. package/src/connections/types.ts +0 -84
  137. package/src/gateway/connection-manager.ts +0 -291
  138. package/src/gateway/index.ts +0 -698
  139. package/src/gateway/job-router.ts +0 -201
  140. package/src/gateway-main.ts +0 -200
  141. package/src/index.ts +0 -41
  142. package/src/infrastructure/queue/index.ts +0 -12
  143. package/src/infrastructure/queue/queue-producer.ts +0 -148
  144. package/src/infrastructure/queue/redis-queue.ts +0 -361
  145. package/src/infrastructure/queue/types.ts +0 -133
  146. package/src/infrastructure/redis/system-message-limiter.ts +0 -94
  147. package/src/interactions/config-request-store.ts +0 -198
  148. package/src/interactions.ts +0 -363
  149. package/src/lobu.ts +0 -311
  150. package/src/metrics/prometheus.ts +0 -159
  151. package/src/modules/module-system.ts +0 -179
  152. package/src/orchestration/base-deployment-manager.ts +0 -900
  153. package/src/orchestration/deployment-utils.ts +0 -98
  154. package/src/orchestration/impl/docker-deployment.ts +0 -620
  155. package/src/orchestration/impl/embedded-deployment.ts +0 -268
  156. package/src/orchestration/impl/index.ts +0 -8
  157. package/src/orchestration/impl/k8s/deployment.ts +0 -1061
  158. package/src/orchestration/impl/k8s/helpers.ts +0 -610
  159. package/src/orchestration/impl/k8s/index.ts +0 -1
  160. package/src/orchestration/index.ts +0 -333
  161. package/src/orchestration/message-consumer.ts +0 -584
  162. package/src/orchestration/scheduled-wakeup.ts +0 -704
  163. package/src/permissions/approval-policy.ts +0 -36
  164. package/src/permissions/grant-store.ts +0 -219
  165. package/src/platform/file-handler.ts +0 -66
  166. package/src/platform/link-buttons.ts +0 -57
  167. package/src/platform/renderer-utils.ts +0 -44
  168. package/src/platform/response-renderer.ts +0 -84
  169. package/src/platform/unified-thread-consumer.ts +0 -187
  170. package/src/platform.ts +0 -318
  171. package/src/proxy/http-proxy.ts +0 -752
  172. package/src/proxy/proxy-manager.ts +0 -81
  173. package/src/proxy/secret-proxy.ts +0 -402
  174. package/src/proxy/token-refresh-job.ts +0 -143
  175. package/src/routes/internal/audio.ts +0 -141
  176. package/src/routes/internal/device-auth.ts +0 -652
  177. package/src/routes/internal/files.ts +0 -226
  178. package/src/routes/internal/history.ts +0 -69
  179. package/src/routes/internal/images.ts +0 -127
  180. package/src/routes/internal/interactions.ts +0 -84
  181. package/src/routes/internal/middleware.ts +0 -23
  182. package/src/routes/internal/schedule.ts +0 -226
  183. package/src/routes/internal/types.ts +0 -22
  184. package/src/routes/openapi-auto.ts +0 -239
  185. package/src/routes/public/agent-access.ts +0 -23
  186. package/src/routes/public/agent-config.ts +0 -675
  187. package/src/routes/public/agent-history.ts +0 -422
  188. package/src/routes/public/agent-schedules.ts +0 -296
  189. package/src/routes/public/agent.ts +0 -1086
  190. package/src/routes/public/agents.ts +0 -373
  191. package/src/routes/public/channels.ts +0 -191
  192. package/src/routes/public/cli-auth.ts +0 -896
  193. package/src/routes/public/connections.ts +0 -574
  194. package/src/routes/public/landing.ts +0 -16
  195. package/src/routes/public/oauth.ts +0 -147
  196. package/src/routes/public/settings-auth.ts +0 -104
  197. package/src/routes/public/slack.ts +0 -173
  198. package/src/routes/shared/agent-ownership.ts +0 -101
  199. package/src/routes/shared/token-verifier.ts +0 -34
  200. package/src/services/bedrock-model-catalog.ts +0 -217
  201. package/src/services/bedrock-openai-service.ts +0 -658
  202. package/src/services/core-services.ts +0 -1072
  203. package/src/services/image-generation-service.ts +0 -257
  204. package/src/services/instruction-service.ts +0 -318
  205. package/src/services/mcp-registry.ts +0 -94
  206. package/src/services/platform-helpers.ts +0 -287
  207. package/src/services/session-manager.ts +0 -262
  208. package/src/services/settings-resolver.ts +0 -74
  209. package/src/services/system-config-resolver.ts +0 -89
  210. package/src/services/system-skills-service.ts +0 -229
  211. package/src/services/transcription-service.ts +0 -684
  212. package/src/session.ts +0 -110
  213. package/src/spaces/index.ts +0 -1
  214. package/src/spaces/space-resolver.ts +0 -17
  215. package/src/stores/in-memory-agent-store.ts +0 -403
  216. package/src/stores/redis-agent-store.ts +0 -279
  217. package/src/utils/public-url.ts +0 -44
  218. package/src/utils/rate-limiter.ts +0 -94
  219. package/tsconfig.json +0 -33
@@ -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 Tempo endpoint for distributed tracing
484
- const tempoEndpoint = process.env.TEMPO_ENDPOINT;
485
- if (tempoEndpoint) {
486
- envVars.TEMPO_ENDPOINT = tempoEndpoint;
487
- try {
488
- const tempoUrl = new URL(tempoEndpoint);
489
- envVars.NO_PROXY = `${envVars.NO_PROXY},${tempoUrl.hostname}`;
490
- } catch {
491
- envVars.NO_PROXY = `${envVars.NO_PROXY},lobu-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
- }