@lobu/gateway 3.0.9 → 3.0.13

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