@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,201 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createLogger } from "@lobu/core";
|
|
4
|
+
import type { IMessageQueue } from "../infrastructure/queue";
|
|
5
|
+
import type { ISessionManager } from "../session";
|
|
6
|
+
import type { WorkerConnectionManager } from "./connection-manager";
|
|
7
|
+
|
|
8
|
+
const logger = createLogger("worker-job-router");
|
|
9
|
+
|
|
10
|
+
interface PendingJob {
|
|
11
|
+
resolve: (value: unknown) => void;
|
|
12
|
+
reject: (error: Error) => void;
|
|
13
|
+
timeout: NodeJS.Timeout;
|
|
14
|
+
jobId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Routes jobs from queues to workers via SSE connections
|
|
19
|
+
* Manages job acknowledgments and timeouts
|
|
20
|
+
*/
|
|
21
|
+
export class WorkerJobRouter {
|
|
22
|
+
private pendingJobs: Map<string, PendingJob> = new Map(); // In-memory timeouts only
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private queue: IMessageQueue,
|
|
26
|
+
private connectionManager: WorkerConnectionManager,
|
|
27
|
+
_sessionManager: ISessionManager
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a worker to receive jobs from its deployment queue
|
|
32
|
+
* Each worker listens on its own queue: thread_message_{deploymentName}
|
|
33
|
+
*
|
|
34
|
+
* Note: This is idempotent - BullMQ's queue.work() handles duplicate registrations gracefully.
|
|
35
|
+
* Safe to call multiple times (e.g., on worker reconnection or gateway restart).
|
|
36
|
+
*/
|
|
37
|
+
async registerWorker(deploymentName: string): Promise<void> {
|
|
38
|
+
const queueName = `thread_message_${deploymentName}`;
|
|
39
|
+
|
|
40
|
+
// Create queue if it doesn't exist
|
|
41
|
+
await this.queue.createQueue(queueName);
|
|
42
|
+
|
|
43
|
+
// Register job handler (idempotent - BullMQ handles duplicates)
|
|
44
|
+
// Start paused so jobs aren't consumed before the SSE connection is live.
|
|
45
|
+
// The caller must call resumeWorker() after SSE connects.
|
|
46
|
+
await this.queue.work(
|
|
47
|
+
queueName,
|
|
48
|
+
async (job: unknown) => {
|
|
49
|
+
await this.handleJob(deploymentName, job);
|
|
50
|
+
},
|
|
51
|
+
{ startPaused: true }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
logger.info(`Registered worker for queue ${queueName}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Pause the BullMQ worker when SSE connection is lost
|
|
59
|
+
* This prevents jobs from being processed when worker can't receive them
|
|
60
|
+
*/
|
|
61
|
+
async pauseWorker(deploymentName: string): Promise<void> {
|
|
62
|
+
const queueName = `thread_message_${deploymentName}`;
|
|
63
|
+
await this.queue.pauseWorker(queueName);
|
|
64
|
+
logger.info(
|
|
65
|
+
`Paused job processing for ${deploymentName} - worker disconnected`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resume the BullMQ worker when SSE connection is established
|
|
71
|
+
* Jobs will now be processed and sent to the worker
|
|
72
|
+
*/
|
|
73
|
+
async resumeWorker(deploymentName: string): Promise<void> {
|
|
74
|
+
const queueName = `thread_message_${deploymentName}`;
|
|
75
|
+
await this.queue.resumeWorker(queueName);
|
|
76
|
+
logger.info(
|
|
77
|
+
`Resumed job processing for ${deploymentName} - worker connected`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handle a job from the queue and route it to the worker.
|
|
83
|
+
*
|
|
84
|
+
* Sends the job via SSE and waits for a delivery receipt from the worker.
|
|
85
|
+
* If the worker doesn't acknowledge within the timeout, the job is retried
|
|
86
|
+
* by BullMQ. This prevents jobs from being silently lost when sent to a
|
|
87
|
+
* stale SSE connection (e.g., after a container dies without cleanly closing TCP).
|
|
88
|
+
*/
|
|
89
|
+
private async handleJob(deploymentName: string, job: unknown): Promise<void> {
|
|
90
|
+
const connection = this.connectionManager.getConnection(deploymentName);
|
|
91
|
+
|
|
92
|
+
if (!connection) {
|
|
93
|
+
logger.warn(
|
|
94
|
+
`No connection for deployment ${deploymentName}, job will be retried`
|
|
95
|
+
);
|
|
96
|
+
throw new Error("Worker not connected");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extract job data and ID
|
|
100
|
+
const jobData = (job as { data?: unknown }).data;
|
|
101
|
+
const jobId =
|
|
102
|
+
(job as { id?: string }).id ||
|
|
103
|
+
`job-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
104
|
+
|
|
105
|
+
// Send job to worker via SSE with jobId wrapped in payload
|
|
106
|
+
const jobPayload =
|
|
107
|
+
typeof jobData === "object" && jobData !== null
|
|
108
|
+
? { payload: jobData, jobId: jobId }
|
|
109
|
+
: { payload: { data: jobData }, jobId: jobId };
|
|
110
|
+
|
|
111
|
+
const sent = this.connectionManager.sendSSE(
|
|
112
|
+
connection.writer,
|
|
113
|
+
"job",
|
|
114
|
+
jobPayload
|
|
115
|
+
);
|
|
116
|
+
if (!sent) {
|
|
117
|
+
logger.warn(
|
|
118
|
+
`SSE write failed for job ${jobId} to ${deploymentName}, will retry`
|
|
119
|
+
);
|
|
120
|
+
throw new Error("SSE write failed - worker connection may be dead");
|
|
121
|
+
}
|
|
122
|
+
this.connectionManager.touchConnection(deploymentName);
|
|
123
|
+
|
|
124
|
+
// Wait for delivery receipt from worker. If the SSE connection is stale
|
|
125
|
+
// (container dead but TCP not yet closed), the worker will never ack and
|
|
126
|
+
// BullMQ will retry the job after the timeout.
|
|
127
|
+
await this.awaitDeliveryReceipt(jobId, deploymentName);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wait for the worker to acknowledge receipt of a job.
|
|
132
|
+
* Rejects after timeout so BullMQ retries the job.
|
|
133
|
+
*/
|
|
134
|
+
private awaitDeliveryReceipt(
|
|
135
|
+
jobId: string,
|
|
136
|
+
deploymentName: string
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
return new Promise<void>((resolve, reject) => {
|
|
139
|
+
const timeout = setTimeout(() => {
|
|
140
|
+
this.pendingJobs.delete(jobId);
|
|
141
|
+
logger.warn(
|
|
142
|
+
`Job ${jobId} delivery receipt timeout - worker ${deploymentName} may be dead`
|
|
143
|
+
);
|
|
144
|
+
reject(
|
|
145
|
+
new Error(
|
|
146
|
+
`Delivery receipt timeout for job ${jobId} - worker may be dead`
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
}, 5000); // 5 second timeout for delivery receipt
|
|
150
|
+
|
|
151
|
+
this.pendingJobs.set(jobId, {
|
|
152
|
+
resolve: () => {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
resolve();
|
|
155
|
+
},
|
|
156
|
+
reject: (err: Error) => {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
reject(err);
|
|
159
|
+
},
|
|
160
|
+
timeout,
|
|
161
|
+
jobId,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Acknowledge job completion from worker
|
|
168
|
+
* Called when worker sends HTTP response
|
|
169
|
+
*/
|
|
170
|
+
acknowledgeJob(jobId: string): void {
|
|
171
|
+
const pendingJob = this.pendingJobs.get(jobId);
|
|
172
|
+
if (pendingJob) {
|
|
173
|
+
clearTimeout(pendingJob.timeout);
|
|
174
|
+
pendingJob.resolve(undefined);
|
|
175
|
+
this.pendingJobs.delete(jobId);
|
|
176
|
+
logger.debug(`Job ${jobId} acknowledged`);
|
|
177
|
+
} else {
|
|
178
|
+
logger.warn(`Received acknowledgment for unknown job ${jobId}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get number of pending jobs
|
|
184
|
+
*/
|
|
185
|
+
getPendingJobCount(): number {
|
|
186
|
+
return this.pendingJobs.size;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Shutdown job router
|
|
191
|
+
*/
|
|
192
|
+
shutdown(): void {
|
|
193
|
+
// Reject all pending jobs
|
|
194
|
+
for (const [jobId, pendingJob] of this.pendingJobs.entries()) {
|
|
195
|
+
clearTimeout(pendingJob.timeout);
|
|
196
|
+
pendingJob.reject(new Error("Job router shutting down"));
|
|
197
|
+
logger.debug(`Rejected pending job ${jobId} due to shutdown`);
|
|
198
|
+
}
|
|
199
|
+
this.pendingJobs.clear();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AgentAccessStore,
|
|
5
|
+
type AgentConfigStore,
|
|
6
|
+
type AgentConnectionStore,
|
|
7
|
+
createLogger,
|
|
8
|
+
type SystemSkillEntry,
|
|
9
|
+
} from "@lobu/core";
|
|
10
|
+
import type { GatewayConfig } from "./config";
|
|
11
|
+
import { type PlatformAdapter, platformRegistry } from "./platform";
|
|
12
|
+
import { UnifiedThreadResponseConsumer } from "./platform/unified-thread-consumer";
|
|
13
|
+
import { CoreServices } from "./services/core-services";
|
|
14
|
+
|
|
15
|
+
const logger = createLogger("gateway");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main Gateway class that orchestrates all platform adapters
|
|
19
|
+
*
|
|
20
|
+
* Architecture:
|
|
21
|
+
* - CoreServices: Platform-agnostic services (Redis, MCP, Anthropic)
|
|
22
|
+
* - PlatformAdapters: Platform-specific integrations (Slack, Discord, etc.)
|
|
23
|
+
*
|
|
24
|
+
* Lifecycle:
|
|
25
|
+
* 1. Gateway initializes CoreServices
|
|
26
|
+
* 2. Platforms register themselves via registerPlatform()
|
|
27
|
+
* 3. Gateway calls initialize() on each platform with CoreServices
|
|
28
|
+
* 4. Gateway calls start() on each platform
|
|
29
|
+
*/
|
|
30
|
+
export interface GatewayOptions {
|
|
31
|
+
/** Agent settings + metadata store. Defaults to InMemoryAgentStore. */
|
|
32
|
+
configStore?: AgentConfigStore;
|
|
33
|
+
/** Connections + channel bindings store. Defaults to InMemoryAgentStore. */
|
|
34
|
+
connectionStore?: AgentConnectionStore;
|
|
35
|
+
/** Grants + user-agent associations store. Defaults to InMemoryAgentStore. */
|
|
36
|
+
accessStore?: AgentAccessStore;
|
|
37
|
+
/** Provide system skills programmatically (skips file loading). */
|
|
38
|
+
systemSkills?: SystemSkillEntry[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Gateway {
|
|
42
|
+
private coreServices: CoreServices;
|
|
43
|
+
private platforms: Map<string, PlatformAdapter> = new Map();
|
|
44
|
+
private unifiedConsumer?: UnifiedThreadResponseConsumer;
|
|
45
|
+
private isRunning = false;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly config: GatewayConfig,
|
|
49
|
+
options?: GatewayOptions
|
|
50
|
+
) {
|
|
51
|
+
this.coreServices = new CoreServices(config, {
|
|
52
|
+
configStore: options?.configStore,
|
|
53
|
+
connectionStore: options?.connectionStore,
|
|
54
|
+
accessStore: options?.accessStore,
|
|
55
|
+
systemSkills: options?.systemSkills,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register a platform adapter
|
|
61
|
+
* Platforms register themselves via dependency injection
|
|
62
|
+
*
|
|
63
|
+
* @param platform - Platform adapter to register
|
|
64
|
+
* @returns This gateway for chaining
|
|
65
|
+
*/
|
|
66
|
+
registerPlatform(platform: PlatformAdapter): this {
|
|
67
|
+
if (this.platforms.has(platform.name)) {
|
|
68
|
+
throw new Error(`Platform ${platform.name} is already registered`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.platforms.set(platform.name, platform);
|
|
72
|
+
// Also register in global platform registry for deployment managers
|
|
73
|
+
platformRegistry.register(platform);
|
|
74
|
+
logger.debug(`Platform registered: ${platform.name}`);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start the gateway
|
|
80
|
+
* 1. Initialize core services
|
|
81
|
+
* 2. Initialize all platforms
|
|
82
|
+
* 3. Register instruction providers from platforms
|
|
83
|
+
* 4. Start all platforms
|
|
84
|
+
*/
|
|
85
|
+
async start(): Promise<void> {
|
|
86
|
+
logger.debug("Starting gateway...");
|
|
87
|
+
|
|
88
|
+
// 1. Initialize core services (Redis, MCP, Anthropic, etc.)
|
|
89
|
+
await this.coreServices.initialize();
|
|
90
|
+
|
|
91
|
+
// 2. Initialize each platform with core services
|
|
92
|
+
for (const [name, platform] of this.platforms) {
|
|
93
|
+
logger.debug(`Initializing platform: ${name}`);
|
|
94
|
+
await platform.initialize(this.coreServices);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Register instruction providers from platforms
|
|
98
|
+
const instructionService = this.coreServices.getInstructionService();
|
|
99
|
+
if (instructionService) {
|
|
100
|
+
for (const [name, platform] of this.platforms) {
|
|
101
|
+
if (platform.getInstructionProvider) {
|
|
102
|
+
const provider = platform.getInstructionProvider();
|
|
103
|
+
if (provider) {
|
|
104
|
+
instructionService.registerPlatformProvider(name, provider);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4. Start all platforms
|
|
111
|
+
for (const [name, platform] of this.platforms) {
|
|
112
|
+
logger.debug(`Starting platform: ${name}`);
|
|
113
|
+
await platform.start();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 5. Start unified thread response consumer
|
|
117
|
+
// Single consumer routes responses to platforms via registry
|
|
118
|
+
this.unifiedConsumer = new UnifiedThreadResponseConsumer(
|
|
119
|
+
this.coreServices.getQueue(),
|
|
120
|
+
platformRegistry
|
|
121
|
+
);
|
|
122
|
+
await this.unifiedConsumer.start();
|
|
123
|
+
|
|
124
|
+
this.isRunning = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Stop the gateway gracefully
|
|
129
|
+
* 1. Stop unified consumer if running
|
|
130
|
+
* 2. Stop all platforms
|
|
131
|
+
* 3. Shutdown core services
|
|
132
|
+
*/
|
|
133
|
+
async stop(): Promise<void> {
|
|
134
|
+
logger.info("Stopping gateway...");
|
|
135
|
+
|
|
136
|
+
// Stop unified consumer if running
|
|
137
|
+
if (this.unifiedConsumer) {
|
|
138
|
+
logger.info("Stopping unified thread response consumer");
|
|
139
|
+
try {
|
|
140
|
+
await this.unifiedConsumer.stop();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.error("Failed to stop unified consumer:", error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Stop all platforms
|
|
147
|
+
for (const [name, platform] of this.platforms) {
|
|
148
|
+
logger.info(`Stopping platform: ${name}`);
|
|
149
|
+
try {
|
|
150
|
+
await platform.stop();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error(`Failed to stop platform ${name}:`, error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Shutdown core services
|
|
157
|
+
await this.coreServices.shutdown();
|
|
158
|
+
|
|
159
|
+
this.isRunning = false;
|
|
160
|
+
logger.info("✅ Gateway stopped");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get gateway status
|
|
165
|
+
*/
|
|
166
|
+
getStatus(): {
|
|
167
|
+
isRunning: boolean;
|
|
168
|
+
platforms: string[];
|
|
169
|
+
config: Partial<GatewayConfig>;
|
|
170
|
+
} {
|
|
171
|
+
return {
|
|
172
|
+
isRunning: this.isRunning,
|
|
173
|
+
platforms: Array.from(this.platforms.keys()),
|
|
174
|
+
config: {
|
|
175
|
+
queues: this.config.queues,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get core services (for platform adapters during initialization)
|
|
182
|
+
*/
|
|
183
|
+
getCoreServices(): CoreServices {
|
|
184
|
+
return this.coreServices;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get platform registry (for routes that need to access platform adapters)
|
|
189
|
+
*/
|
|
190
|
+
getPlatformRegistry() {
|
|
191
|
+
return platformRegistry;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get unified thread response consumer (for wiring Chat SDK response bridge)
|
|
196
|
+
*/
|
|
197
|
+
getUnifiedConsumer() {
|
|
198
|
+
return this.unifiedConsumer;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main entry point for Lobu Gateway
|
|
5
|
+
*
|
|
6
|
+
* When run directly (CLI mode): starts the gateway server.
|
|
7
|
+
* When imported as a library (embedded mode): exports Gateway, config builders,
|
|
8
|
+
* and the Hono app factory for mounting on a host server.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ── Primary API ─────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export { Lobu, type LobuAgentConfig, type LobuConfig } from "./lobu";
|
|
14
|
+
|
|
15
|
+
// ── Advanced (for custom setups) ────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export { createGatewayApp, startGatewayServer } from "./cli/gateway";
|
|
18
|
+
export {
|
|
19
|
+
type AgentConfig,
|
|
20
|
+
buildGatewayConfig,
|
|
21
|
+
type GatewayConfig,
|
|
22
|
+
} from "./config";
|
|
23
|
+
export { Gateway, type GatewayOptions } from "./gateway-main";
|
|
24
|
+
export { CoreServices } from "./services/core-services";
|
|
25
|
+
export { InMemoryAgentStore } from "./stores/in-memory-agent-store";
|
|
26
|
+
|
|
27
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
AgentAccessStore,
|
|
31
|
+
AgentConfigStore,
|
|
32
|
+
AgentConnectionStore,
|
|
33
|
+
AgentMetadata,
|
|
34
|
+
AgentSettings,
|
|
35
|
+
AgentStore,
|
|
36
|
+
} from "@lobu/core";
|
|
37
|
+
|
|
38
|
+
// ── CLI mode (run directly, not when imported as library) ───────────────────
|
|
39
|
+
if (require.main === module) {
|
|
40
|
+
import("./cli");
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue infrastructure
|
|
3
|
+
* Redis-based message queue using BullMQ
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { QueueProducer } from "./queue-producer";
|
|
7
|
+
export { RedisQueue, type RedisQueueConfig } from "./redis-queue";
|
|
8
|
+
export type {
|
|
9
|
+
IMessageQueue,
|
|
10
|
+
QueueJob,
|
|
11
|
+
ThreadResponsePayload,
|
|
12
|
+
} from "./types";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AgentMcpConfig,
|
|
5
|
+
createLogger,
|
|
6
|
+
type NetworkConfig,
|
|
7
|
+
type NixConfig,
|
|
8
|
+
} from "@lobu/core";
|
|
9
|
+
import type { IMessageQueue } from "./types";
|
|
10
|
+
|
|
11
|
+
const logger = createLogger("queue-producer");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Job type for queue messages
|
|
15
|
+
* - message: Standard agent message execution
|
|
16
|
+
* - exec: Direct command execution in sandbox
|
|
17
|
+
*/
|
|
18
|
+
export type JobType = "message" | "exec";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Universal message payload for all queue stages
|
|
22
|
+
* Used by: Slack events → Queue → Message Consumer → Job Router → Worker
|
|
23
|
+
*/
|
|
24
|
+
export interface MessagePayload {
|
|
25
|
+
// Core identifiers (used by gateway for routing)
|
|
26
|
+
userId: string; // Platform user ID
|
|
27
|
+
conversationId: string; // Conversation ID (must be root conversation ID)
|
|
28
|
+
messageId: string; // Individual message ID
|
|
29
|
+
channelId: string; // Platform channel ID
|
|
30
|
+
teamId: string; // Team/workspace ID (required for all platforms)
|
|
31
|
+
agentId: string; // Agent/session ID for isolation (universal identifier)
|
|
32
|
+
|
|
33
|
+
// Bot & platform info (passed through to worker)
|
|
34
|
+
botId: string; // Bot identifier
|
|
35
|
+
platform: string; // Platform name
|
|
36
|
+
|
|
37
|
+
// Message content (used by worker)
|
|
38
|
+
messageText: string; // The actual message text
|
|
39
|
+
|
|
40
|
+
// Platform-specific data (used by worker for context)
|
|
41
|
+
platformMetadata: Record<string, any>;
|
|
42
|
+
|
|
43
|
+
// Agent configuration (used by worker)
|
|
44
|
+
agentOptions: Record<string, any>;
|
|
45
|
+
|
|
46
|
+
// Per-agent network configuration for sandbox isolation
|
|
47
|
+
networkConfig?: NetworkConfig;
|
|
48
|
+
|
|
49
|
+
// Per-agent MCP configuration (additive to global MCPs)
|
|
50
|
+
mcpConfig?: AgentMcpConfig;
|
|
51
|
+
|
|
52
|
+
// Nix environment configuration for agent workspace
|
|
53
|
+
nixConfig?: NixConfig;
|
|
54
|
+
|
|
55
|
+
// Job type (default: "message")
|
|
56
|
+
jobType?: JobType;
|
|
57
|
+
|
|
58
|
+
// Exec-specific fields (only used when jobType === "exec")
|
|
59
|
+
execId?: string; // Unique ID for exec job (for response routing)
|
|
60
|
+
execCommand?: string; // Command to execute
|
|
61
|
+
execCwd?: string; // Working directory for command
|
|
62
|
+
execEnv?: Record<string, string>; // Additional environment variables
|
|
63
|
+
execTimeout?: number; // Timeout in milliseconds
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Queue producer for dispatching messages to Redis queues
|
|
68
|
+
* Handles both direct_message and thread_message queues with bot isolation
|
|
69
|
+
*/
|
|
70
|
+
export class QueueProducer {
|
|
71
|
+
private queue: IMessageQueue;
|
|
72
|
+
private isInitialized = false;
|
|
73
|
+
|
|
74
|
+
constructor(queue: IMessageQueue) {
|
|
75
|
+
this.queue = queue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize the queue producer
|
|
80
|
+
* Creates required queues
|
|
81
|
+
*/
|
|
82
|
+
async start(): Promise<void> {
|
|
83
|
+
try {
|
|
84
|
+
// Create the messages queue if it doesn't exist
|
|
85
|
+
await this.queue.createQueue("messages");
|
|
86
|
+
this.isInitialized = true;
|
|
87
|
+
logger.debug("Queue producer initialized");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error("Failed to initialize queue producer:", error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Stop the queue producer (no-op since queue lifecycle is managed externally)
|
|
96
|
+
*/
|
|
97
|
+
async stop(): Promise<void> {
|
|
98
|
+
this.isInitialized = false;
|
|
99
|
+
logger.debug("Queue producer stopped");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Enqueue any message (direct or thread) to the single 'messages' queue
|
|
104
|
+
* Orchestrator will determine if it needs to create a deployment or route to existing thread
|
|
105
|
+
*/
|
|
106
|
+
async enqueueMessage(
|
|
107
|
+
payload: MessagePayload,
|
|
108
|
+
options?: {
|
|
109
|
+
priority?: number;
|
|
110
|
+
retryLimit?: number;
|
|
111
|
+
retryDelay?: number;
|
|
112
|
+
expireInSeconds?: number;
|
|
113
|
+
}
|
|
114
|
+
): Promise<string> {
|
|
115
|
+
if (!this.isInitialized) {
|
|
116
|
+
throw new Error("Queue producer is not initialized");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// All messages go to the single 'messages' queue
|
|
121
|
+
const jobId = await this.queue.send("messages", payload, {
|
|
122
|
+
priority: options?.priority || 0,
|
|
123
|
+
retryLimit: options?.retryLimit || 3,
|
|
124
|
+
retryDelay: options?.retryDelay || 30,
|
|
125
|
+
expireInSeconds: options?.expireInSeconds || 300, // 5 minutes = 300 seconds
|
|
126
|
+
singletonKey: `message-${payload.platform}-${payload.channelId}-${payload.conversationId}-${String(payload.messageId || Date.now()).replace(/:/g, "-")}`, // Prevent duplicates within canonical conversation identity
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
logger.info(
|
|
130
|
+
`Enqueued message job ${jobId} for user ${payload.userId}, conversation ${payload.conversationId}`
|
|
131
|
+
);
|
|
132
|
+
return jobId || "job-sent";
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error(
|
|
135
|
+
`Failed to enqueue message for user ${payload.userId}:`,
|
|
136
|
+
error
|
|
137
|
+
);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if producer is initialized
|
|
144
|
+
*/
|
|
145
|
+
isHealthy(): boolean {
|
|
146
|
+
return this.isInitialized && this.queue.isHealthy();
|
|
147
|
+
}
|
|
148
|
+
}
|