@lobu/gateway 3.0.5 → 3.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/agent-config-routes.test.ts +254 -0
- package/src/__tests__/agent-history-routes.test.ts +72 -0
- package/src/__tests__/agent-routes.test.ts +68 -0
- package/src/__tests__/agent-schedules-routes.test.ts +59 -0
- package/src/__tests__/agent-settings-store.test.ts +323 -0
- package/src/__tests__/chat-instance-manager-slack.test.ts +204 -0
- package/src/__tests__/chat-response-bridge.test.ts +131 -0
- package/src/__tests__/config-memory-plugins.test.ts +92 -0
- package/src/__tests__/config-request-store.test.ts +127 -0
- package/src/__tests__/connection-routes.test.ts +144 -0
- package/src/__tests__/core-services-store-selection.test.ts +92 -0
- package/src/__tests__/docker-deployment.test.ts +1211 -0
- package/src/__tests__/embedded-deployment.test.ts +342 -0
- package/src/__tests__/grant-store.test.ts +148 -0
- package/src/__tests__/http-proxy.test.ts +281 -0
- package/src/__tests__/instruction-service.test.ts +37 -0
- package/src/__tests__/link-buttons.test.ts +112 -0
- package/src/__tests__/lobu.test.ts +32 -0
- package/src/__tests__/mcp-config-service.test.ts +347 -0
- package/src/__tests__/mcp-proxy.test.ts +696 -0
- package/src/__tests__/message-handler-bridge.test.ts +17 -0
- package/src/__tests__/model-selection.test.ts +172 -0
- package/src/__tests__/oauth-templates.test.ts +39 -0
- package/src/__tests__/platform-adapter-slack-send.test.ts +114 -0
- package/src/__tests__/platform-helpers-model-resolution.test.ts +253 -0
- package/src/__tests__/provider-inheritance.test.ts +212 -0
- package/src/__tests__/routes/cli-auth.test.ts +337 -0
- package/src/__tests__/routes/interactions.test.ts +121 -0
- package/src/__tests__/secret-proxy.test.ts +85 -0
- package/src/__tests__/session-manager.test.ts +572 -0
- package/src/__tests__/setup.ts +133 -0
- package/src/__tests__/skill-and-mcp-registry.test.ts +203 -0
- package/src/__tests__/slack-routes.test.ts +161 -0
- package/src/__tests__/system-config-resolver.test.ts +75 -0
- package/src/__tests__/system-message-limiter.test.ts +89 -0
- package/src/__tests__/system-skills-service.test.ts +362 -0
- package/src/__tests__/transcription-service.test.ts +222 -0
- package/src/__tests__/utils/rate-limiter.test.ts +102 -0
- package/src/__tests__/worker-connection-manager.test.ts +497 -0
- package/src/__tests__/worker-job-router.test.ts +722 -0
- package/src/api/index.ts +1 -0
- package/src/api/platform.ts +292 -0
- package/src/api/response-renderer.ts +157 -0
- package/src/auth/agent-metadata-store.ts +168 -0
- package/src/auth/api-auth-middleware.ts +69 -0
- package/src/auth/api-key-provider-module.ts +213 -0
- package/src/auth/base-provider-module.ts +201 -0
- package/src/auth/chatgpt/chatgpt-oauth-module.ts +185 -0
- package/src/auth/chatgpt/device-code-client.ts +218 -0
- package/src/auth/chatgpt/index.ts +1 -0
- package/src/auth/claude/oauth-module.ts +280 -0
- package/src/auth/cli/token-service.ts +249 -0
- package/src/auth/external/client.ts +560 -0
- package/src/auth/external/device-code-client.ts +225 -0
- package/src/auth/mcp/config-service.ts +392 -0
- package/src/auth/mcp/proxy.ts +1088 -0
- package/src/auth/mcp/string-substitution.ts +17 -0
- package/src/auth/mcp/tool-cache.ts +90 -0
- package/src/auth/oauth/base-client.ts +267 -0
- package/src/auth/oauth/client.ts +153 -0
- package/src/auth/oauth/credentials.ts +7 -0
- package/src/auth/oauth/providers.ts +69 -0
- package/src/auth/oauth/state-store.ts +150 -0
- package/src/auth/oauth-templates.ts +179 -0
- package/src/auth/provider-catalog.ts +220 -0
- package/src/auth/provider-model-options.ts +41 -0
- package/src/auth/settings/agent-settings-store.ts +565 -0
- package/src/auth/settings/auth-profiles-manager.ts +216 -0
- package/src/auth/settings/index.ts +12 -0
- package/src/auth/settings/model-preference-store.ts +52 -0
- package/src/auth/settings/model-selection.ts +135 -0
- package/src/auth/settings/resolved-settings-view.ts +298 -0
- package/src/auth/settings/template-utils.ts +44 -0
- package/src/auth/settings/token-service.ts +88 -0
- package/src/auth/system-env-store.ts +98 -0
- package/src/auth/user-agents-store.ts +68 -0
- package/src/channels/binding-service.ts +214 -0
- package/src/channels/index.ts +4 -0
- package/src/cli/gateway.ts +1304 -0
- package/src/cli/index.ts +74 -0
- package/src/commands/built-in-commands.ts +80 -0
- package/src/commands/command-dispatcher.ts +94 -0
- package/src/commands/command-reply-adapters.ts +27 -0
- package/src/config/file-loader.ts +618 -0
- package/src/config/index.ts +588 -0
- package/src/config/network-allowlist.ts +71 -0
- package/src/connections/chat-instance-manager.ts +1284 -0
- package/src/connections/chat-response-bridge.ts +618 -0
- package/src/connections/index.ts +7 -0
- package/src/connections/interaction-bridge.ts +831 -0
- package/src/connections/message-handler-bridge.ts +415 -0
- package/src/connections/platform-auth-methods.ts +15 -0
- package/src/connections/types.ts +84 -0
- package/src/gateway/connection-manager.ts +291 -0
- package/src/gateway/index.ts +700 -0
- package/src/gateway/job-router.ts +201 -0
- package/src/gateway-main.ts +200 -0
- package/src/index.ts +41 -0
- package/src/infrastructure/queue/index.ts +12 -0
- package/src/infrastructure/queue/queue-producer.ts +148 -0
- package/src/infrastructure/queue/redis-queue.ts +361 -0
- package/src/infrastructure/queue/types.ts +133 -0
- package/src/infrastructure/redis/system-message-limiter.ts +94 -0
- package/src/interactions/config-request-store.ts +198 -0
- package/src/interactions.ts +363 -0
- package/src/lobu.ts +311 -0
- package/src/metrics/prometheus.ts +159 -0
- package/src/modules/module-system.ts +179 -0
- package/src/orchestration/base-deployment-manager.ts +900 -0
- package/src/orchestration/deployment-utils.ts +98 -0
- package/src/orchestration/impl/docker-deployment.ts +620 -0
- package/src/orchestration/impl/embedded-deployment.ts +268 -0
- package/src/orchestration/impl/index.ts +8 -0
- package/src/orchestration/impl/k8s/deployment.ts +1061 -0
- package/src/orchestration/impl/k8s/helpers.ts +610 -0
- package/src/orchestration/impl/k8s/index.ts +1 -0
- package/src/orchestration/index.ts +333 -0
- package/src/orchestration/message-consumer.ts +584 -0
- package/src/orchestration/scheduled-wakeup.ts +704 -0
- package/src/permissions/approval-policy.ts +36 -0
- package/src/permissions/grant-store.ts +219 -0
- package/src/platform/file-handler.ts +66 -0
- package/src/platform/link-buttons.ts +57 -0
- package/src/platform/renderer-utils.ts +44 -0
- package/src/platform/response-renderer.ts +84 -0
- package/src/platform/unified-thread-consumer.ts +187 -0
- package/src/platform.ts +318 -0
- package/src/proxy/http-proxy.ts +752 -0
- package/src/proxy/proxy-manager.ts +81 -0
- package/src/proxy/secret-proxy.ts +402 -0
- package/src/proxy/token-refresh-job.ts +143 -0
- package/src/routes/internal/audio.ts +141 -0
- package/src/routes/internal/device-auth.ts +566 -0
- package/src/routes/internal/files.ts +226 -0
- package/src/routes/internal/history.ts +69 -0
- package/src/routes/internal/images.ts +127 -0
- package/src/routes/internal/interactions.ts +84 -0
- package/src/routes/internal/middleware.ts +23 -0
- package/src/routes/internal/schedule.ts +226 -0
- package/src/routes/internal/types.ts +22 -0
- package/src/routes/openapi-auto.ts +239 -0
- package/src/routes/public/agent-access.ts +23 -0
- package/src/routes/public/agent-config.ts +675 -0
- package/src/routes/public/agent-history.ts +422 -0
- package/src/routes/public/agent-schedules.ts +296 -0
- package/src/routes/public/agent.ts +1086 -0
- package/src/routes/public/agents.ts +373 -0
- package/src/routes/public/channels.ts +191 -0
- package/src/routes/public/cli-auth.ts +883 -0
- package/src/routes/public/connections.ts +574 -0
- package/src/routes/public/landing.ts +16 -0
- package/src/routes/public/oauth.ts +147 -0
- package/src/routes/public/settings-auth.ts +104 -0
- package/src/routes/public/slack.ts +173 -0
- package/src/routes/shared/agent-ownership.ts +101 -0
- package/src/routes/shared/token-verifier.ts +34 -0
- package/src/services/core-services.ts +1053 -0
- package/src/services/image-generation-service.ts +257 -0
- package/src/services/instruction-service.ts +318 -0
- package/src/services/mcp-registry.ts +94 -0
- package/src/services/platform-helpers.ts +287 -0
- package/src/services/session-manager.ts +262 -0
- package/src/services/settings-resolver.ts +74 -0
- package/src/services/system-config-resolver.ts +90 -0
- package/src/services/system-skills-service.ts +229 -0
- package/src/services/transcription-service.ts +684 -0
- package/src/session.ts +110 -0
- package/src/spaces/index.ts +1 -0
- package/src/spaces/space-resolver.ts +17 -0
- package/src/stores/in-memory-agent-store.ts +403 -0
- package/src/stores/redis-agent-store.ts +279 -0
- package/src/utils/public-url.ts +44 -0
- package/src/utils/rate-limiter.ts +94 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createLogger } from "@lobu/core";
|
|
4
|
+
|
|
5
|
+
const logger = createLogger("worker-connection-manager");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SSE Writer interface - abstracts the response object for SSE
|
|
9
|
+
*/
|
|
10
|
+
export interface SSEWriter {
|
|
11
|
+
write(data: string): boolean;
|
|
12
|
+
end(): void;
|
|
13
|
+
onClose(callback: () => void): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WorkerConnection {
|
|
17
|
+
deploymentName: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
conversationId: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
writer: SSEWriter;
|
|
22
|
+
lastActivity: number;
|
|
23
|
+
lastPing: number;
|
|
24
|
+
httpUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Manages SSE connections from workers
|
|
29
|
+
* Handles connection lifecycle, heartbeats, and cleanup
|
|
30
|
+
*/
|
|
31
|
+
export class WorkerConnectionManager {
|
|
32
|
+
private connections: Map<string, WorkerConnection> = new Map();
|
|
33
|
+
private agentDeployments: Map<string, Set<string>> = new Map();
|
|
34
|
+
private heartbeatInterval: NodeJS.Timeout;
|
|
35
|
+
private cleanupInterval: NodeJS.Timeout;
|
|
36
|
+
private useLocalhost: boolean;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
const mode = process.env.DEPLOYMENT_MODE || "";
|
|
40
|
+
this.useLocalhost = mode !== "kubernetes" && mode !== "k8s";
|
|
41
|
+
// Send heartbeat pings every 30 seconds
|
|
42
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeats(), 30000);
|
|
43
|
+
|
|
44
|
+
// Cleanup stale connections every 30 seconds
|
|
45
|
+
this.cleanupInterval = setInterval(
|
|
46
|
+
() => this.cleanupStaleConnections(),
|
|
47
|
+
30000
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a new worker connection
|
|
53
|
+
*/
|
|
54
|
+
addConnection(
|
|
55
|
+
deploymentName: string,
|
|
56
|
+
userId: string,
|
|
57
|
+
conversationId: string,
|
|
58
|
+
agentId: string,
|
|
59
|
+
writer: SSEWriter,
|
|
60
|
+
httpPort?: number
|
|
61
|
+
): void {
|
|
62
|
+
// In embedded/Docker mode workers run in-process, so use localhost.
|
|
63
|
+
// In Kubernetes mode each worker is a separate pod addressable by name.
|
|
64
|
+
const httpHost = this.useLocalhost ? "127.0.0.1" : deploymentName;
|
|
65
|
+
const httpUrl = httpPort ? `http://${httpHost}:${httpPort}` : undefined;
|
|
66
|
+
|
|
67
|
+
const connection: WorkerConnection = {
|
|
68
|
+
deploymentName,
|
|
69
|
+
userId,
|
|
70
|
+
conversationId,
|
|
71
|
+
agentId,
|
|
72
|
+
writer,
|
|
73
|
+
lastActivity: Date.now(),
|
|
74
|
+
lastPing: Date.now(),
|
|
75
|
+
httpUrl,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.connections.set(deploymentName, connection);
|
|
79
|
+
|
|
80
|
+
// Maintain agentId → deployments reverse index
|
|
81
|
+
if (!this.agentDeployments.has(agentId)) {
|
|
82
|
+
this.agentDeployments.set(agentId, new Set());
|
|
83
|
+
}
|
|
84
|
+
this.agentDeployments.get(agentId)?.add(deploymentName);
|
|
85
|
+
|
|
86
|
+
// Send initial connection event
|
|
87
|
+
this.sendSSE(writer, "connected", {
|
|
88
|
+
deploymentName,
|
|
89
|
+
userId,
|
|
90
|
+
conversationId,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
`Worker ${deploymentName} connected (user: ${userId}, agent: ${agentId}, conversation: ${conversationId})`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Remove a worker connection
|
|
100
|
+
*/
|
|
101
|
+
removeConnection(deploymentName: string, expectedWriter?: SSEWriter): void {
|
|
102
|
+
const connection = this.connections.get(deploymentName);
|
|
103
|
+
if (connection) {
|
|
104
|
+
if (expectedWriter && connection.writer !== expectedWriter) {
|
|
105
|
+
logger.debug(
|
|
106
|
+
`Skipping disconnect for ${deploymentName} because a newer SSE writer is active`
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clean up reverse index
|
|
112
|
+
const deployments = this.agentDeployments.get(connection.agentId);
|
|
113
|
+
if (deployments) {
|
|
114
|
+
deployments.delete(deploymentName);
|
|
115
|
+
if (deployments.size === 0) {
|
|
116
|
+
this.agentDeployments.delete(connection.agentId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
connection.writer.end();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Connection may already be closed
|
|
124
|
+
logger.debug(
|
|
125
|
+
`Failed to close connection for ${deploymentName}:`,
|
|
126
|
+
error
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
this.connections.delete(deploymentName);
|
|
130
|
+
logger.info(`Worker ${deploymentName} disconnected`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get a worker connection
|
|
136
|
+
*/
|
|
137
|
+
getConnection(deploymentName: string): WorkerConnection | undefined {
|
|
138
|
+
return this.connections.get(deploymentName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if a worker is connected
|
|
143
|
+
*/
|
|
144
|
+
isConnected(deploymentName: string): boolean {
|
|
145
|
+
return this.connections.has(deploymentName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update connection activity timestamp
|
|
150
|
+
*/
|
|
151
|
+
touchConnection(deploymentName: string): void {
|
|
152
|
+
const connection = this.connections.get(deploymentName);
|
|
153
|
+
if (connection) {
|
|
154
|
+
connection.lastActivity = Date.now();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Send SSE event to a worker
|
|
160
|
+
*/
|
|
161
|
+
sendSSE(writer: SSEWriter, event: string, data: unknown): boolean {
|
|
162
|
+
try {
|
|
163
|
+
// Combine into single write to avoid buffering issues
|
|
164
|
+
// Format: event: <event>\ndata: <json>\n\n
|
|
165
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
166
|
+
const success = writer.write(message);
|
|
167
|
+
|
|
168
|
+
if (!success) {
|
|
169
|
+
logger.warn(
|
|
170
|
+
`[SSE] Response stream buffer full for event ${event}, data: ${JSON.stringify(data).substring(0, 100)}`
|
|
171
|
+
);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
`[SSE] Successfully sent ${event} event, data: ${JSON.stringify(data).substring(0, 200)}`
|
|
177
|
+
);
|
|
178
|
+
return true;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logger.error(`[SSE] Failed to send SSE event ${event}:`, error);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Send heartbeat pings to all connected workers
|
|
187
|
+
*/
|
|
188
|
+
private sendHeartbeats(): void {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
|
|
191
|
+
for (const [deploymentName, connection] of this.connections.entries()) {
|
|
192
|
+
try {
|
|
193
|
+
this.sendSSE(connection.writer, "ping", { timestamp: now });
|
|
194
|
+
connection.lastPing = now;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.warn(`Failed to send ping to ${deploymentName}:`, error);
|
|
197
|
+
// Connection might be dead, will be cleaned up by cleanup check
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cleanup stale connections (>10 minutes without activity)
|
|
204
|
+
*/
|
|
205
|
+
private cleanupStaleConnections(): void {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
// Increase timeout to support long-running Claude sessions
|
|
208
|
+
// Configurable via WORKER_STALE_TIMEOUT_MINUTES env var (default: 10 minutes)
|
|
209
|
+
const timeoutMinutes = parseInt(
|
|
210
|
+
process.env.WORKER_STALE_TIMEOUT_MINUTES || "10",
|
|
211
|
+
10
|
|
212
|
+
);
|
|
213
|
+
const staleThreshold = timeoutMinutes * 60 * 1000;
|
|
214
|
+
|
|
215
|
+
for (const [deploymentName, connection] of this.connections.entries()) {
|
|
216
|
+
if (now - connection.lastActivity > staleThreshold) {
|
|
217
|
+
logger.info(
|
|
218
|
+
`Cleaning up stale connection: ${deploymentName} (no activity for ${Math.round((now - connection.lastActivity) / 1000)}s)`
|
|
219
|
+
);
|
|
220
|
+
this.removeConnection(deploymentName);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get all deployment names for a given agentId
|
|
227
|
+
*/
|
|
228
|
+
getDeploymentsForAgent(agentId: string): string[] {
|
|
229
|
+
const deployments = this.agentDeployments.get(agentId);
|
|
230
|
+
return deployments ? Array.from(deployments) : [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Send an SSE event to all connected workers for a given agentId.
|
|
235
|
+
* Partial failures are logged but don't block.
|
|
236
|
+
*/
|
|
237
|
+
notifyAgent(agentId: string, event: string, data: unknown): void {
|
|
238
|
+
const deployments = this.getDeploymentsForAgent(agentId);
|
|
239
|
+
if (deployments.length === 0) {
|
|
240
|
+
logger.debug(
|
|
241
|
+
`No active deployments for agent ${agentId}, skipping ${event} notification`
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logger.info(
|
|
247
|
+
`Sending ${event} to ${deployments.length} deployment(s) for agent ${agentId}`
|
|
248
|
+
);
|
|
249
|
+
for (const deploymentName of deployments) {
|
|
250
|
+
const connection = this.connections.get(deploymentName);
|
|
251
|
+
if (connection) {
|
|
252
|
+
this.sendSSE(connection.writer, event, data);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the HTTP URL for a worker serving the given agentId.
|
|
259
|
+
* Returns the httpUrl of the first connected deployment for the agent.
|
|
260
|
+
*/
|
|
261
|
+
getHttpUrl(agentId: string): string | undefined {
|
|
262
|
+
const deployments = this.getDeploymentsForAgent(agentId);
|
|
263
|
+
for (const deploymentName of deployments) {
|
|
264
|
+
const connection = this.connections.get(deploymentName);
|
|
265
|
+
if (connection?.httpUrl) {
|
|
266
|
+
return connection.httpUrl;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get all active connection names
|
|
274
|
+
*/
|
|
275
|
+
getActiveConnections(): string[] {
|
|
276
|
+
return Array.from(this.connections.keys());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Shutdown connection manager
|
|
281
|
+
*/
|
|
282
|
+
shutdown(): void {
|
|
283
|
+
clearInterval(this.heartbeatInterval);
|
|
284
|
+
clearInterval(this.cleanupInterval);
|
|
285
|
+
|
|
286
|
+
// Close all connections
|
|
287
|
+
for (const deploymentName of this.connections.keys()) {
|
|
288
|
+
this.removeConnection(deploymentName);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|