@tokagent/tokagentos 2.0.29 → 2.0.30
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 +1 -1
- package/scaffold-patches/packages/agent/src/api/plugin-routes.ts +1889 -0
- package/scaffold-patches/packages/agent/src/api/server.ts +4509 -0
- package/scaffold-patches/packages/agent/src/api/trigger-routes.ts +942 -0
- package/scaffold-patches/packages/agent/src/runtime/core-plugins.ts +4 -0
- package/scaffold-patches/packages/agent/src/triggers/runtime.ts +955 -0
- package/scaffold-patches/packages/app-core/src/api/client-agent.ts +2755 -0
- package/scaffold-patches/packages/app-core/src/components/pages/AutomationsView.tsx +446 -26
- package/scaffold-patches/packages/app-core/src/components/pages/SettingsView.tsx +155 -0
- package/scaffold-patches/packages/shared/src/onboarding-presets.characters.ts +16 -16
- package/templates/fullstack-app/package.json +9 -5
- package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/messages-proxy-routes.ts +114 -3
- package/templates/fullstack-app/plugins/plugin-web-fetch/build.ts +35 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/package.json +37 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/src/index.ts +471 -0
- package/templates/fullstack-app/plugins/plugin-web-fetch/tsconfig.json +20 -0
- package/templates/fullstack-app/scripts/ensure-plugin-builds.mjs +1 -0
- package/templates/fullstack-app/scripts/verify-llm-plugins.mjs +122 -0
- package/templates-manifest.json +1 -1
|
@@ -0,0 +1,4509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API server for the Eliza Control UI.
|
|
3
|
+
*
|
|
4
|
+
* Exposes HTTP endpoints that the UI frontend expects, backed by the
|
|
5
|
+
* elizaOS AgentRuntime. Default port: 2138. In dev mode, the Vite UI
|
|
6
|
+
* dev server proxies /api and /ws here (see eliza/packages/app-core/scripts/dev-ui.mjs).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
// dns/promises moved to server-helpers-mcp.ts
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
|
|
15
|
+
function tokenMatches(expected: string, provided: string): boolean {
|
|
16
|
+
const expectedBuf = Buffer.from(expected);
|
|
17
|
+
const providedBuf = Buffer.from(provided);
|
|
18
|
+
return (
|
|
19
|
+
expectedBuf.length === providedBuf.length &&
|
|
20
|
+
crypto.timingSafeEqual(expectedBuf, providedBuf)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAX_BODY_BYTES = 1024 * 1024; // 1 MB
|
|
25
|
+
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
// Discord local routes extracted to @elizaos/plugin-discord (setup-routes.ts)
|
|
29
|
+
import { DropService, setElizaMakerDropService } from "@elizaos/app-elizamaker";
|
|
30
|
+
import { handleKnowledgeRoutes } from "@elizaos/app-knowledge/routes";
|
|
31
|
+
import {
|
|
32
|
+
normalizeJsonRpcUrl,
|
|
33
|
+
probeJsonRpcEndpoint,
|
|
34
|
+
TxService,
|
|
35
|
+
} from "@elizaos/app-steward/api/tx-service";
|
|
36
|
+
import {
|
|
37
|
+
ensurePrivyWalletsForCustomUser,
|
|
38
|
+
isPrivyWalletProvisioningEnabled,
|
|
39
|
+
} from "@elizaos/app-steward/services/privy-wallets";
|
|
40
|
+
import { wireCoordinatorBridgesWhenReady } from "@elizaos/app-task-coordinator/api/coordinator-wiring";
|
|
41
|
+
// Phase 2 extraction: LifeOps routes → app-lifeops/src/routes/plugin.ts (lifeopsPlugin)
|
|
42
|
+
// import { handleWalletTradeExecuteRoute } from "./wallet-trade-routes.js";
|
|
43
|
+
// import {
|
|
44
|
+
// loadWalletTradingProfile,
|
|
45
|
+
// recordWalletTradeLedgerEntry,
|
|
46
|
+
// updateWalletTradeLedgerEntryStatus,
|
|
47
|
+
// } from "./wallet-trading-profile.js";
|
|
48
|
+
// Phase 2 extraction: Website-blocker routes → app-lifeops/src/routes/plugin.ts (lifeopsPlugin)
|
|
49
|
+
import {
|
|
50
|
+
handleTrainingRoutes,
|
|
51
|
+
handleTrajectoryRoute,
|
|
52
|
+
} from "@elizaos/app-training/routes";
|
|
53
|
+
import {
|
|
54
|
+
type AgentRuntime,
|
|
55
|
+
type IAgentRuntime,
|
|
56
|
+
logger,
|
|
57
|
+
type Route,
|
|
58
|
+
stringToUuid,
|
|
59
|
+
type UUID,
|
|
60
|
+
} from "@elizaos/core";
|
|
61
|
+
import {
|
|
62
|
+
getStylePresets,
|
|
63
|
+
normalizeCharacterLanguage,
|
|
64
|
+
resolveApiBindHost,
|
|
65
|
+
resolveServerOnlyPort,
|
|
66
|
+
resolveStylePresetByAvatarIndex,
|
|
67
|
+
} from "@elizaos/shared";
|
|
68
|
+
import { type WebSocket, WebSocketServer } from "ws";
|
|
69
|
+
import { getGlobalAwarenessRegistry } from "../awareness/registry.js";
|
|
70
|
+
import {
|
|
71
|
+
type ElizaConfig,
|
|
72
|
+
loadElizaConfig,
|
|
73
|
+
saveElizaConfig,
|
|
74
|
+
} from "../config/config.js";
|
|
75
|
+
import { resolveModelsCacheDir, resolveStateDir } from "../config/paths.js";
|
|
76
|
+
import { isStreamingDestinationConfigured } from "../config/plugin-auto-enable.js";
|
|
77
|
+
import { CharacterSchema } from "../config/zod-schema.js";
|
|
78
|
+
// ONBOARDING_CLOUD_PROVIDER_OPTIONS, ONBOARDING_PROVIDER_CATALOG moved to server-helpers-config.ts
|
|
79
|
+
import { createIntegrationTelemetrySpan } from "../diagnostics/integration-observability.js";
|
|
80
|
+
import { validateX402Startup } from "../middleware/x402/startup-validator.ts";
|
|
81
|
+
import { resolveDefaultAgentWorkspaceDir } from "../providers/workspace.js";
|
|
82
|
+
import {
|
|
83
|
+
type AgentEventPayloadLike,
|
|
84
|
+
type AgentEventServiceLike,
|
|
85
|
+
getAgentEventService,
|
|
86
|
+
} from "../runtime/agent-event-service.js";
|
|
87
|
+
import { classifyRegistryPluginRelease } from "../runtime/release-plugin-policy.js";
|
|
88
|
+
import {
|
|
89
|
+
AUDIT_EVENT_TYPES,
|
|
90
|
+
AUDIT_SEVERITIES,
|
|
91
|
+
getAuditFeedSize,
|
|
92
|
+
queryAuditFeed,
|
|
93
|
+
subscribeAuditFeed,
|
|
94
|
+
} from "../security/audit-log.js";
|
|
95
|
+
import { isLoopbackHost } from "../security/network-policy.js";
|
|
96
|
+
import {
|
|
97
|
+
AgentExportError,
|
|
98
|
+
estimateExportSize,
|
|
99
|
+
exportAgent,
|
|
100
|
+
importAgent,
|
|
101
|
+
} from "../services/agent-export.js";
|
|
102
|
+
import { AppManager } from "../services/app-manager.js";
|
|
103
|
+
import { registerClientChatSendHandler } from "../services/client-chat-sender.js";
|
|
104
|
+
import { createConfigPluginManager } from "../services/config-plugin-manager.js";
|
|
105
|
+
import {
|
|
106
|
+
type CoreManagerLike,
|
|
107
|
+
isCoreManagerLike,
|
|
108
|
+
isPluginManagerLike,
|
|
109
|
+
type PluginManagerLike,
|
|
110
|
+
} from "../services/plugin-manager-types.js";
|
|
111
|
+
// signal-pairing: SignalPairingSession, sanitizeAccountId, signalLogout extracted to @elizaos/plugin-signal
|
|
112
|
+
import { signalAuthExists } from "../services/signal-pairing.js";
|
|
113
|
+
import { streamManager } from "../services/stream-manager.js";
|
|
114
|
+
import {
|
|
115
|
+
clearTelegramAccountAuthState,
|
|
116
|
+
clearTelegramAccountSession,
|
|
117
|
+
TelegramAccountAuthSession,
|
|
118
|
+
telegramAccountAuthStateExists,
|
|
119
|
+
telegramAccountSessionExists,
|
|
120
|
+
} from "../services/telegram-account-auth.js";
|
|
121
|
+
import {
|
|
122
|
+
sanitizeAccountId as sanitizeWhatsAppAccountId,
|
|
123
|
+
WhatsAppPairingSession,
|
|
124
|
+
whatsappAuthExists,
|
|
125
|
+
whatsappLogout,
|
|
126
|
+
} from "../services/whatsapp-pairing.js";
|
|
127
|
+
// Telegram account auth: moved to @elizaos/plugin-telegram (account-setup-routes + account-auth-service).
|
|
128
|
+
// WhatsApp pairing: route handlers moved to @elizaos/plugin-whatsapp.
|
|
129
|
+
import {
|
|
130
|
+
executeTriggerTask,
|
|
131
|
+
getTriggerHealthSnapshot,
|
|
132
|
+
getTriggerLimit,
|
|
133
|
+
listTriggerTasks,
|
|
134
|
+
readTriggerConfig,
|
|
135
|
+
readTriggerRuns,
|
|
136
|
+
TRIGGER_TASK_NAME,
|
|
137
|
+
TRIGGER_TASK_TAGS,
|
|
138
|
+
taskToTriggerSummary,
|
|
139
|
+
triggersFeatureEnabled,
|
|
140
|
+
} from "../triggers/runtime.js";
|
|
141
|
+
import {
|
|
142
|
+
buildTriggerConfig,
|
|
143
|
+
buildTriggerMetadata,
|
|
144
|
+
DISABLED_TRIGGER_INTERVAL_MS,
|
|
145
|
+
normalizeTriggerDraft,
|
|
146
|
+
} from "../triggers/scheduling.js";
|
|
147
|
+
import { parseClampedInteger } from "../utils/number-parsing.js";
|
|
148
|
+
import { handleAccountsRoutes } from "./accounts-routes.js";
|
|
149
|
+
import { handleAgentAdminRoutes } from "./agent-admin-routes.js";
|
|
150
|
+
import { handleAgentLifecycleRoutes } from "./agent-lifecycle-routes.js";
|
|
151
|
+
import { detectRuntimeModel, resolveProviderFromModel } from "./agent-model.js";
|
|
152
|
+
import { handleAgentStatusRoutes } from "./agent-status-routes.js";
|
|
153
|
+
import { handleAgentTransferRoutes } from "./agent-transfer-routes.js";
|
|
154
|
+
import { handleAppPackageRoutes } from "./app-package-routes.js";
|
|
155
|
+
import { handleAppsRoutes } from "./apps-routes.js";
|
|
156
|
+
import { handleAuthRoutes } from "./auth-routes.js";
|
|
157
|
+
import { handleAvatarRoutes } from "./avatar-routes.js";
|
|
158
|
+
import {
|
|
159
|
+
handleBlueBubblesRoute,
|
|
160
|
+
resolveBlueBubblesWebhookPath,
|
|
161
|
+
} from "./bluebubbles-routes.js";
|
|
162
|
+
import { handleBrowserWorkspaceRoutes } from "./browser-workspace-routes.js";
|
|
163
|
+
import { handleBugReportRoutes } from "./bug-report-routes.js";
|
|
164
|
+
import { handleCharacterRoutes } from "./character-routes.js";
|
|
165
|
+
import {
|
|
166
|
+
handleChatRoutes,
|
|
167
|
+
initSse as initSseFromChatRoutes,
|
|
168
|
+
writeSseJson as writeSseJsonFromChatRoutes,
|
|
169
|
+
} from "./chat-routes.js";
|
|
170
|
+
import { handleCloudBillingRoute } from "./cloud-billing-routes.js";
|
|
171
|
+
import { handleCloudCompatRoute } from "./cloud-compat-routes.js";
|
|
172
|
+
import { isCloudProvisionedContainer } from "./cloud-provisioning.js";
|
|
173
|
+
import { handleCloudRelayRoute } from "./cloud-relay-routes.js";
|
|
174
|
+
import { type CloudRouteState, handleCloudRoute } from "./cloud-routes.js";
|
|
175
|
+
import { handleCloudStatusRoutes } from "./cloud-status-routes.js";
|
|
176
|
+
import { handleCodingAgentsFallback } from "./coding-agents-fallback-routes.js";
|
|
177
|
+
import { handleConfigRoutes } from "./config-routes.js";
|
|
178
|
+
import { ConnectorHealthMonitor } from "./connector-health.js";
|
|
179
|
+
import { handleConnectorRoutes } from "./connector-routes.js";
|
|
180
|
+
import { credTypesForConnector } from "@elizaos/shared";
|
|
181
|
+
import { extractConversationMetadataFromRoom } from "./conversation-metadata.js";
|
|
182
|
+
import { handleConversationRoutes } from "./conversation-routes.js";
|
|
183
|
+
import { handleCuratedSkillsRoutes } from "./curated-skills-routes.js";
|
|
184
|
+
import { handleDatabaseRoute } from "./database.js";
|
|
185
|
+
import { handleDiagnosticsRoutes } from "./diagnostics-routes.js";
|
|
186
|
+
import { handleExperienceRoutes } from "./experience-routes.js";
|
|
187
|
+
import { handleHealthRoutes } from "./health-routes.js";
|
|
188
|
+
import {
|
|
189
|
+
readJsonBody as parseJsonBody,
|
|
190
|
+
type ReadJsonBodyOptions,
|
|
191
|
+
readRequestBody,
|
|
192
|
+
sendJson,
|
|
193
|
+
sendJsonError,
|
|
194
|
+
} from "./http-helpers.js";
|
|
195
|
+
// iMessage routes extracted to @elizaos/plugin-imessage setup-routes.ts (Plugin.routes)
|
|
196
|
+
// import { handleIMessageRoute } from "./imessage-routes.js";
|
|
197
|
+
import { handleInboxRoute } from "./inbox-routes.js";
|
|
198
|
+
import { handleMcpRoutes } from "./mcp-routes.js";
|
|
199
|
+
import { pushWithBatchEvict } from "./memory-bounds.js";
|
|
200
|
+
import { handleMemoryRoutes } from "./memory-routes.js";
|
|
201
|
+
import { handleMiscRoutes } from "./misc-routes.js";
|
|
202
|
+
import { handleModelsRoutes } from "./models-routes.js";
|
|
203
|
+
import { tryHandleMusicPlayerStatusFallback } from "./music-player-route-fallback.js";
|
|
204
|
+
import { handleOnboardingRoutes } from "./onboarding-routes.js";
|
|
205
|
+
import type { PTYService } from "./parse-action-block.js";
|
|
206
|
+
import { handlePermissionRoutes } from "./permissions-routes.js";
|
|
207
|
+
import { handlePermissionsExtraRoutes } from "./permissions-routes-extra.js";
|
|
208
|
+
import { handlePluginRoutes } from "./plugin-routes.js";
|
|
209
|
+
import {
|
|
210
|
+
type ClassifyContext,
|
|
211
|
+
createColdStrategy,
|
|
212
|
+
createHotStrategy,
|
|
213
|
+
defaultClassifier,
|
|
214
|
+
DefaultRuntimeOperationManager,
|
|
215
|
+
getDefaultHealthChecker,
|
|
216
|
+
getDefaultRepository,
|
|
217
|
+
type RuntimeOperationManager,
|
|
218
|
+
} from "../runtime/operations/index.js";
|
|
219
|
+
import {
|
|
220
|
+
resolvePreferredProviderId,
|
|
221
|
+
resolvePrimaryModel,
|
|
222
|
+
} from "../runtime/eliza.js";
|
|
223
|
+
import { handleProviderSwitchRoutes } from "./provider-switch-routes.js";
|
|
224
|
+
import { handleRegistryRoutes } from "./registry-routes.js";
|
|
225
|
+
import { RegistryService } from "./registry-service.js";
|
|
226
|
+
import { handleRelationshipsRoutes } from "./relationships-routes.js";
|
|
227
|
+
import {
|
|
228
|
+
isPublicRuntimePluginRoute,
|
|
229
|
+
tryHandleRuntimePluginRoute,
|
|
230
|
+
} from "./runtime-plugin-routes.js";
|
|
231
|
+
import { handleSandboxRoute } from "./sandbox-routes.js";
|
|
232
|
+
import {
|
|
233
|
+
cloneWithoutBlockedObjectKeys,
|
|
234
|
+
decodePathComponent,
|
|
235
|
+
getErrorMessage,
|
|
236
|
+
hasPersistedOnboardingState,
|
|
237
|
+
isUuidLike,
|
|
238
|
+
patchTouchesProviderSelection,
|
|
239
|
+
} from "./server-helpers.js";
|
|
240
|
+
// signal-routes: handleSignalRoute dispatch extracted to @elizaos/plugin-signal (setup-routes.ts)
|
|
241
|
+
import { applySignalQrOverride } from "./signal-routes.js";
|
|
242
|
+
import { discoverSkills } from "./skill-discovery-helpers.js";
|
|
243
|
+
import { handleSkillsRoutes } from "./skills-routes.js";
|
|
244
|
+
import { handleSubscriptionRoutes } from "./subscription-routes.js";
|
|
245
|
+
import { handleTelegramAccountRoute } from "./telegram-account-routes.js";
|
|
246
|
+
import { handleTriggerRoutes } from "./trigger-routes.js";
|
|
247
|
+
import { handleTtsRoutes } from "./tts-routes.js";
|
|
248
|
+
import { handleUpdateRoutes } from "./update-routes.js";
|
|
249
|
+
import {
|
|
250
|
+
// Balance/import/generate helpers moved to @elizaos/app-steward plugin routes.
|
|
251
|
+
// generateWalletKeys, setSolanaWalletEnv moved to server-helpers-config.ts
|
|
252
|
+
getWalletAddresses,
|
|
253
|
+
initStewardWalletCache,
|
|
254
|
+
} from "./wallet.js";
|
|
255
|
+
// Wallet dispatch moved to @elizaos/app-steward plugin routes.
|
|
256
|
+
// import { handleWalletBscRoutes } from "./wallet-bsc-routes.js";
|
|
257
|
+
import {
|
|
258
|
+
EVM_PLUGIN_PACKAGE,
|
|
259
|
+
resolveWalletAutomationMode as resolveAgentAutomationModeFromConfig,
|
|
260
|
+
resolveWalletCapabilityStatus,
|
|
261
|
+
} from "./wallet-capability.js";
|
|
262
|
+
import { handleWalletRoutes } from "./wallet-routes.js";
|
|
263
|
+
import { resolveWalletRpcReadiness } from "./wallet-rpc.js";
|
|
264
|
+
import {
|
|
265
|
+
applyWhatsAppQrOverride,
|
|
266
|
+
handleWhatsAppRoute,
|
|
267
|
+
} from "./whatsapp-routes.js";
|
|
268
|
+
import { handleWorkbenchRoutes } from "./workbench-routes.js";
|
|
269
|
+
import { handleXRelayRoute } from "./x-relay-routes.js";
|
|
270
|
+
|
|
271
|
+
export {
|
|
272
|
+
executeFallbackParsedActions,
|
|
273
|
+
extractXmlParams,
|
|
274
|
+
type FallbackParsedAction,
|
|
275
|
+
inferBalanceChainFromText,
|
|
276
|
+
isBalanceIntent,
|
|
277
|
+
maybeHandleDirectBinanceSkillRequest,
|
|
278
|
+
parseFallbackActionBlocks,
|
|
279
|
+
shouldForceCheckBalanceFallback,
|
|
280
|
+
} from "./binance-skill-helpers.js";
|
|
281
|
+
|
|
282
|
+
type OnboardingRouteArg = Parameters<typeof handleOnboardingRoutes>[0];
|
|
283
|
+
type AgentStatusRouteArg = Parameters<typeof handleAgentStatusRoutes>[0];
|
|
284
|
+
type TtsRouteArg = Parameters<typeof handleTtsRoutes>[0];
|
|
285
|
+
type PermissionsExtraRouteArg = Parameters<
|
|
286
|
+
typeof handlePermissionsExtraRoutes
|
|
287
|
+
>[0];
|
|
288
|
+
type ConversationRouteArg = Parameters<typeof handleConversationRoutes>[0];
|
|
289
|
+
type ChatRouteArg = Parameters<typeof handleChatRoutes>[0];
|
|
290
|
+
type WorkbenchRouteArg = Parameters<typeof handleWorkbenchRoutes>[0];
|
|
291
|
+
// LifeOpsRouteArg removed — routes extracted to lifeopsPlugin
|
|
292
|
+
type MiscRouteArg = Parameters<typeof handleMiscRoutes>[0];
|
|
293
|
+
|
|
294
|
+
export {
|
|
295
|
+
isClientVisibleNoResponse,
|
|
296
|
+
isNoResponsePlaceholder,
|
|
297
|
+
stripAssistantStageDirections,
|
|
298
|
+
} from "./chat-text-helpers.js";
|
|
299
|
+
|
|
300
|
+
// Re-export helper functions from server-helpers.ts for backwards compatibility
|
|
301
|
+
export {
|
|
302
|
+
buildChatAttachments,
|
|
303
|
+
buildUserMessages,
|
|
304
|
+
buildWalletActionNotExecutedReply,
|
|
305
|
+
cloneWithoutBlockedObjectKeys,
|
|
306
|
+
decodePathComponent,
|
|
307
|
+
findOwnPackageRoot,
|
|
308
|
+
getErrorMessage,
|
|
309
|
+
hasBlockedObjectKeyDeep,
|
|
310
|
+
IMAGE_ONLY_CHAT_FALLBACK_PROMPT,
|
|
311
|
+
isUuidLike,
|
|
312
|
+
isWalletActionRequiredIntent,
|
|
313
|
+
maybeAugmentChatMessageWithKnowledge,
|
|
314
|
+
maybeAugmentChatMessageWithLanguage,
|
|
315
|
+
maybeAugmentChatMessageWithWalletContext,
|
|
316
|
+
normalizeIncomingChatPrompt,
|
|
317
|
+
persistConversationRoomTitle,
|
|
318
|
+
resolveAppUserName,
|
|
319
|
+
resolveConversationGreetingText,
|
|
320
|
+
resolveWalletModeGuidanceReply,
|
|
321
|
+
trimWalletProgressPrefix,
|
|
322
|
+
validateChatImages,
|
|
323
|
+
WALLET_EXECUTION_INTENT_RE,
|
|
324
|
+
WALLET_PROGRESS_ONLY_RE,
|
|
325
|
+
} from "./server-helpers.js";
|
|
326
|
+
|
|
327
|
+
// NOTE: Internal usage of these functions is handled by individual `import`
|
|
328
|
+
// statements placed where each function was originally defined (see below).
|
|
329
|
+
// The `export { ... } from` above re-exports them for external consumers.
|
|
330
|
+
|
|
331
|
+
import {
|
|
332
|
+
getInventoryProviderOptions,
|
|
333
|
+
getModelOptions,
|
|
334
|
+
getOrFetchAllProviders,
|
|
335
|
+
getOrFetchProvider,
|
|
336
|
+
paramKeyToCategory,
|
|
337
|
+
providerCachePath,
|
|
338
|
+
readProviderCache,
|
|
339
|
+
} from "./model-provider-helpers.js";
|
|
340
|
+
import {
|
|
341
|
+
AGENT_EVENT_ALLOWED_STREAMS,
|
|
342
|
+
aggregateSecrets,
|
|
343
|
+
BLOCKED_ENV_KEYS,
|
|
344
|
+
CONFIG_WRITE_ALLOWED_TOP_KEYS,
|
|
345
|
+
discoverInstalledPlugins,
|
|
346
|
+
discoverPluginsFromManifest,
|
|
347
|
+
getReleaseBundledPluginIds,
|
|
348
|
+
maskValue,
|
|
349
|
+
type PluginEntry,
|
|
350
|
+
} from "./plugin-discovery-helpers.js";
|
|
351
|
+
|
|
352
|
+
const _nodeRequire = createRequire(import.meta.url);
|
|
353
|
+
// Dynamic import (not require) because the plugin is ESM-only and bun's
|
|
354
|
+
// createRequire cannot load ESM packages. Top-level await is settled before
|
|
355
|
+
// any consumer reads the binding.
|
|
356
|
+
let agentOrchestratorCompat: unknown = null;
|
|
357
|
+
try {
|
|
358
|
+
agentOrchestratorCompat = await import("@elizaos/plugin-agent-orchestrator");
|
|
359
|
+
} catch {
|
|
360
|
+
agentOrchestratorCompat = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Re-export for downstream consumers (e.g. @elizaos/app-core)
|
|
364
|
+
export {
|
|
365
|
+
AGENT_EVENT_ALLOWED_STREAMS,
|
|
366
|
+
CONFIG_WRITE_ALLOWED_TOP_KEYS,
|
|
367
|
+
discoverInstalledPlugins,
|
|
368
|
+
discoverPluginsFromManifest,
|
|
369
|
+
findPrimaryEnvKey,
|
|
370
|
+
readBundledPluginPackageMetadata,
|
|
371
|
+
} from "./plugin-discovery-helpers.js";
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Types
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
// ConnectorRouteHandler imported from server-types.ts
|
|
378
|
+
import type { ConnectorRouteHandler } from "./server-types.js";
|
|
379
|
+
|
|
380
|
+
type OrchestratorFallbackRouteHandler = (
|
|
381
|
+
req: http.IncomingMessage,
|
|
382
|
+
res: http.ServerResponse,
|
|
383
|
+
pathname: string,
|
|
384
|
+
method?: string,
|
|
385
|
+
) => Promise<boolean>;
|
|
386
|
+
|
|
387
|
+
interface OrchestratorPluginFallbackModule {
|
|
388
|
+
createCodingAgentRouteHandler?: (
|
|
389
|
+
runtime: AgentRuntime,
|
|
390
|
+
coordinator?: unknown,
|
|
391
|
+
) => OrchestratorFallbackRouteHandler;
|
|
392
|
+
getCoordinator?: (runtime: AgentRuntime) => unknown;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getAgentEventSvc(
|
|
396
|
+
runtime: AgentRuntime | null,
|
|
397
|
+
): AgentEventServiceLike | null {
|
|
398
|
+
return getAgentEventService(runtime);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function requirePluginManager(runtime: AgentRuntime | null): PluginManagerLike {
|
|
402
|
+
const service = runtime?.getService("plugin_manager");
|
|
403
|
+
if (!isPluginManagerLike(service)) {
|
|
404
|
+
throw new Error("Plugin manager service not found");
|
|
405
|
+
}
|
|
406
|
+
return wrapPluginManagerWithLocalFallback(service);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The upstream plugin-plugin-manager has its own registry client that only
|
|
411
|
+
* fetches from GitHub and scans a `plugins/` dir for `elizaos.plugin.json`.
|
|
412
|
+
* Workspace-vendored plugins (under `packages/plugin-*`) are invisible to it.
|
|
413
|
+
* Wrap `installPlugin` so that when the upstream returns "not found in the
|
|
414
|
+
* registry" we retry using our own registry-client (which discovers workspace
|
|
415
|
+
* packages and node_modules symlinks).
|
|
416
|
+
*/
|
|
417
|
+
function wrapPluginManagerWithLocalFallback(
|
|
418
|
+
pm: PluginManagerLike,
|
|
419
|
+
): PluginManagerLike {
|
|
420
|
+
const originalInstall = pm.installPlugin.bind(pm);
|
|
421
|
+
const wrapped: PluginManagerLike = Object.create(pm);
|
|
422
|
+
|
|
423
|
+
wrapped.installPlugin = async (pluginName, onProgress) => {
|
|
424
|
+
const result = await originalInstall(pluginName, onProgress);
|
|
425
|
+
if (
|
|
426
|
+
result.success ||
|
|
427
|
+
!result.error?.includes("not found in the registry")
|
|
428
|
+
) {
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Upstream registry missed it — check Eliza's own local discovery.
|
|
433
|
+
const { getPluginInfo } = await import("../services/registry-client.js");
|
|
434
|
+
const localInfo = await getPluginInfo(pluginName);
|
|
435
|
+
if (!localInfo?.localPath) {
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The plugin is a workspace package — just return success pointing at it.
|
|
440
|
+
// The runtime already resolves it via NODE_PATH / bun workspace links so
|
|
441
|
+
// there is nothing to download; the caller only needs to enable it in
|
|
442
|
+
// config and restart.
|
|
443
|
+
return {
|
|
444
|
+
success: true,
|
|
445
|
+
pluginName: localInfo.name,
|
|
446
|
+
version:
|
|
447
|
+
localInfo.npm.v2Version ?? localInfo.npm.v1Version ?? "workspace",
|
|
448
|
+
installPath: localInfo.localPath,
|
|
449
|
+
requiresRestart: true,
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
return wrapped;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getPluginManagerForState(state: ServerState): PluginManagerLike {
|
|
457
|
+
const service = state.runtime?.getService("plugin_manager");
|
|
458
|
+
if (isPluginManagerLike(service)) {
|
|
459
|
+
return service;
|
|
460
|
+
}
|
|
461
|
+
return createConfigPluginManager(() => state.config);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function requireCoreManager(runtime: AgentRuntime | null): CoreManagerLike {
|
|
465
|
+
const service = runtime?.getService("core_manager");
|
|
466
|
+
if (!isCoreManagerLike(service)) {
|
|
467
|
+
throw new Error("Core manager service not found");
|
|
468
|
+
}
|
|
469
|
+
return service;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const OG_FILENAME = ".og";
|
|
473
|
+
const DELETED_CONVERSATIONS_FILENAME = "deleted-conversations.v1.json";
|
|
474
|
+
const MAX_DELETED_CONVERSATION_IDS = 5000;
|
|
475
|
+
|
|
476
|
+
interface DeletedConversationsStateFile {
|
|
477
|
+
version: 1;
|
|
478
|
+
updatedAt: string;
|
|
479
|
+
ids: string[];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function readDeletedConversationIdsFromState(): Set<string> {
|
|
483
|
+
const filePath = path.join(resolveStateDir(), DELETED_CONVERSATIONS_FILENAME);
|
|
484
|
+
if (!fs.existsSync(filePath)) return new Set();
|
|
485
|
+
try {
|
|
486
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
487
|
+
const parsed = JSON.parse(raw) as Partial<DeletedConversationsStateFile>;
|
|
488
|
+
const ids = Array.isArray(parsed.ids) ? parsed.ids : [];
|
|
489
|
+
return new Set(
|
|
490
|
+
ids
|
|
491
|
+
.map((id) => (typeof id === "string" ? id.trim() : ""))
|
|
492
|
+
.filter((id) => id.length > 0),
|
|
493
|
+
);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
logger.warn(
|
|
496
|
+
`[eliza-api] Failed to read deleted conversations state: ${err instanceof Error ? err.message : String(err)}`,
|
|
497
|
+
);
|
|
498
|
+
return new Set();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function _persistDeletedConversationIdsToState(ids: Set<string>): void {
|
|
503
|
+
const dir = resolveStateDir();
|
|
504
|
+
if (!fs.existsSync(dir)) {
|
|
505
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const normalized = Array.from(ids)
|
|
509
|
+
.map((id) => id.trim())
|
|
510
|
+
.filter((id) => id.length > 0)
|
|
511
|
+
.slice(-MAX_DELETED_CONVERSATION_IDS);
|
|
512
|
+
|
|
513
|
+
const filePath = path.join(dir, DELETED_CONVERSATIONS_FILENAME);
|
|
514
|
+
const tmpFilePath = `${filePath}.${process.pid}.tmp`;
|
|
515
|
+
const payload: DeletedConversationsStateFile = {
|
|
516
|
+
version: 1,
|
|
517
|
+
updatedAt: new Date().toISOString(),
|
|
518
|
+
ids: normalized,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
fs.writeFileSync(tmpFilePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
522
|
+
encoding: "utf-8",
|
|
523
|
+
mode: 0o600,
|
|
524
|
+
});
|
|
525
|
+
fs.renameSync(tmpFilePath, filePath);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function initializeOGCodeInState(): void {
|
|
529
|
+
const dir = resolveStateDir();
|
|
530
|
+
const filePath = path.join(dir, OG_FILENAME);
|
|
531
|
+
if (fs.existsSync(filePath)) return;
|
|
532
|
+
|
|
533
|
+
if (!fs.existsSync(dir)) {
|
|
534
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
535
|
+
}
|
|
536
|
+
fs.writeFileSync(filePath, crypto.randomUUID(), {
|
|
537
|
+
encoding: "utf-8",
|
|
538
|
+
mode: 0o600,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// resolveAppUserName, patchTouchesProviderSelection, resolveConversationGreetingText
|
|
543
|
+
// moved to server-helpers.ts; imported in the consolidated import at the top
|
|
544
|
+
|
|
545
|
+
// AgentStartupDiagnostics, ConversationMeta, ServerState, ShareIngestItem,
|
|
546
|
+
// SkillEntry, LogEntry, StreamEventType, StreamEventEnvelope re-exported from
|
|
547
|
+
// server-types.ts
|
|
548
|
+
export type {
|
|
549
|
+
AgentStartupDiagnostics,
|
|
550
|
+
ConversationMeta,
|
|
551
|
+
LogEntry,
|
|
552
|
+
ServerState,
|
|
553
|
+
ShareIngestItem,
|
|
554
|
+
SkillEntry,
|
|
555
|
+
StreamEventEnvelope,
|
|
556
|
+
StreamEventType,
|
|
557
|
+
} from "./server-types.js";
|
|
558
|
+
|
|
559
|
+
import type {
|
|
560
|
+
AgentStartupDiagnostics,
|
|
561
|
+
ServerState,
|
|
562
|
+
StreamEventEnvelope,
|
|
563
|
+
} from "./server-types.js";
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Package root resolution (for reading bundled plugins.json)
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
// findOwnPackageRoot moved to server-helpers.ts; re-exported in the batch above
|
|
570
|
+
|
|
571
|
+
// Fetch/streaming helpers extracted to server-helpers-fetch.ts
|
|
572
|
+
import {
|
|
573
|
+
fetchWithTimeoutGuard as _fetchWithTimeoutGuard,
|
|
574
|
+
streamResponseBodyWithByteLimit as _streamResponseBodyWithByteLimit,
|
|
575
|
+
isAbortError,
|
|
576
|
+
responseContentLength,
|
|
577
|
+
} from "./server-helpers-fetch.js";
|
|
578
|
+
|
|
579
|
+
export {
|
|
580
|
+
fetchWithTimeoutGuard,
|
|
581
|
+
streamResponseBodyWithByteLimit,
|
|
582
|
+
} from "./server-helpers-fetch.js";
|
|
583
|
+
|
|
584
|
+
const fetchWithTimeoutGuard = _fetchWithTimeoutGuard;
|
|
585
|
+
const streamResponseBodyWithByteLimit = _streamResponseBodyWithByteLimit;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Read and parse a JSON request body with size limits and error handling.
|
|
589
|
+
* Returns null (and sends a 4xx response) if reading or parsing fails.
|
|
590
|
+
*/
|
|
591
|
+
async function readJsonBody<T = Record<string, unknown>>(
|
|
592
|
+
req: http.IncomingMessage,
|
|
593
|
+
res: http.ServerResponse,
|
|
594
|
+
options: ReadJsonBodyOptions = {},
|
|
595
|
+
): Promise<T | null> {
|
|
596
|
+
return parseJsonBody(req, res, {
|
|
597
|
+
maxBytes: MAX_BODY_BYTES,
|
|
598
|
+
...options,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const readBody = (req: http.IncomingMessage): Promise<string> =>
|
|
603
|
+
readRequestBody(req, { maxBytes: MAX_BODY_BYTES }).then(
|
|
604
|
+
(value) => value ?? "",
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
let activeTerminalRunCount = 0;
|
|
608
|
+
|
|
609
|
+
function json(res: http.ServerResponse, data: unknown, status = 200): void {
|
|
610
|
+
sendJson(res, data, status);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function error(res: http.ServerResponse, message: string, status = 400): void {
|
|
614
|
+
sendJsonError(res, message, status);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function isModuleResolutionFailure(err: unknown): boolean {
|
|
618
|
+
if (typeof err !== "object" || err === null) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const code = "code" in err ? (err as NodeJS.ErrnoException).code : undefined;
|
|
622
|
+
if (
|
|
623
|
+
code === "MODULE_NOT_FOUND" ||
|
|
624
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
625
|
+
code === "ERR_PACKAGE_PATH_NOT_EXPORTED"
|
|
626
|
+
) {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
if (!("message" in err) || typeof err.message !== "string") {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return (
|
|
633
|
+
err.message.includes("Cannot find module") ||
|
|
634
|
+
err.message.includes("Cannot find package") ||
|
|
635
|
+
err.message.includes("ERR_MODULE_NOT_FOUND") ||
|
|
636
|
+
err.message.includes('is not defined by "exports"')
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function isWalletBridgeImportFailure(err: unknown): boolean {
|
|
641
|
+
if (isModuleResolutionFailure(err)) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (typeof err !== "object" || err === null) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const code = "code" in err ? (err as NodeJS.ErrnoException).code : undefined;
|
|
648
|
+
if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
if (!("message" in err) || typeof err.message !== "string") {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
return err.message.includes('Unknown file extension ".css"');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
// Static UI serving — extracted to static-file-server.ts
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
import {
|
|
661
|
+
injectApiBaseIntoHtml,
|
|
662
|
+
isAuthProtectedRoute,
|
|
663
|
+
serveStaticUi,
|
|
664
|
+
} from "./static-file-server.js";
|
|
665
|
+
|
|
666
|
+
export { injectApiBaseIntoHtml };
|
|
667
|
+
|
|
668
|
+
// Preserved for backward-compat — unused locally after extraction.
|
|
669
|
+
const _STATIC_MIME: Record<string, string> = {};
|
|
670
|
+
|
|
671
|
+
// (static file serving functions moved to static-file-server.ts)
|
|
672
|
+
|
|
673
|
+
function coerce<T>(value: unknown): T {
|
|
674
|
+
return value as T;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// maybeAugmentChatMessageWithLanguage and getErrorMessage moved to server-helpers.ts;
|
|
678
|
+
// imported in the consolidated import at the top
|
|
679
|
+
|
|
680
|
+
// Knowledge + wallet context augmentation moved to server-helpers.ts;
|
|
681
|
+
// imported in the consolidated import at the top
|
|
682
|
+
|
|
683
|
+
// ChatImageAttachment, image validation, chat attachments, normalizeIncomingChatPrompt,
|
|
684
|
+
// and buildUserMessages moved to server-helpers.ts; re-exported in the top-level block
|
|
685
|
+
// ChatAttachmentWithData re-exported from server-types.ts
|
|
686
|
+
export type { ChatAttachmentWithData } from "./server-types.js";
|
|
687
|
+
|
|
688
|
+
// buildChatAttachments, buildUserMessages, etc. imported in the consolidated import at the top
|
|
689
|
+
|
|
690
|
+
function parseBoundedLimit(rawLimit: string | null, fallback = 15): number {
|
|
691
|
+
return parseClampedInteger(rawLimit, {
|
|
692
|
+
min: 1,
|
|
693
|
+
max: 50,
|
|
694
|
+
fallback,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function sanitizeFavoriteAppList(value: unknown): string[] {
|
|
699
|
+
if (!Array.isArray(value)) return [];
|
|
700
|
+
const seen = new Set<string>();
|
|
701
|
+
const apps: string[] = [];
|
|
702
|
+
for (const item of value) {
|
|
703
|
+
if (typeof item !== "string") continue;
|
|
704
|
+
const trimmed = item.trim();
|
|
705
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
706
|
+
seen.add(trimmed);
|
|
707
|
+
apps.push(trimmed);
|
|
708
|
+
}
|
|
709
|
+
return apps;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function readFavoriteAppsFromConfig(config: ElizaConfig): string[] {
|
|
713
|
+
const ui = (config.ui ?? {}) as Record<string, unknown>;
|
|
714
|
+
return sanitizeFavoriteAppList(ui.favoriteApps);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function writeFavoriteAppsToConfig(
|
|
718
|
+
config: ElizaConfig,
|
|
719
|
+
apps: string[],
|
|
720
|
+
): string[] {
|
|
721
|
+
const sanitized = sanitizeFavoriteAppList(apps);
|
|
722
|
+
const ui = (config.ui ?? {}) as Record<string, unknown>;
|
|
723
|
+
ui.favoriteApps = sanitized;
|
|
724
|
+
config.ui = ui as ElizaConfig["ui"];
|
|
725
|
+
saveElizaConfig(config);
|
|
726
|
+
return sanitized;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Config redaction, skill validation extracted to server-helpers-config.ts
|
|
730
|
+
// isBlockedObjectKey, redactDeep, redactConfigSecrets, isRedactedSecretValue,
|
|
731
|
+
// stripRedactedPlaceholderValuesDeep imported from server-helpers-config.ts above.
|
|
732
|
+
// isBlockedObjectKey alias for local usage:
|
|
733
|
+
const isBlockedObjectKey = isBlockedObjectKeyFromConfig;
|
|
734
|
+
|
|
735
|
+
// MCP validation helpers extracted to server-helpers-mcp.ts
|
|
736
|
+
import {
|
|
737
|
+
resolveMcpServersRejection as _resolveMcpServersRejection,
|
|
738
|
+
resolveMcpTerminalAuthorizationRejection as _resolveMcpTerminalAuthorizationRejection,
|
|
739
|
+
} from "./server-helpers-mcp.js";
|
|
740
|
+
|
|
741
|
+
export {
|
|
742
|
+
resolveMcpServersRejection,
|
|
743
|
+
resolveMcpTerminalAuthorizationRejection,
|
|
744
|
+
validateMcpServerConfig,
|
|
745
|
+
} from "./server-helpers-mcp.js";
|
|
746
|
+
|
|
747
|
+
const resolveMcpServersRejection = _resolveMcpServersRejection;
|
|
748
|
+
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// Onboarding / config helpers — extracted to server-helpers-config.ts
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
import { pickRandomNames } from "../runtime/onboarding-names.js";
|
|
754
|
+
|
|
755
|
+
import {
|
|
756
|
+
applyOnboardingVoicePreset,
|
|
757
|
+
ensureWalletKeysInEnvAndConfig,
|
|
758
|
+
getCloudProviderOptions,
|
|
759
|
+
getProviderOptions,
|
|
760
|
+
isBlockedObjectKey as isBlockedObjectKeyFromConfig,
|
|
761
|
+
isRedactedSecretValue,
|
|
762
|
+
isSafeResetStateDir,
|
|
763
|
+
readUiLanguageHeader,
|
|
764
|
+
redactConfigSecrets,
|
|
765
|
+
redactDeep,
|
|
766
|
+
resolveConfiguredCharacterLanguage,
|
|
767
|
+
resolveDefaultAgentName,
|
|
768
|
+
stripRedactedPlaceholderValuesDeep,
|
|
769
|
+
} from "./server-helpers-config.js";
|
|
770
|
+
|
|
771
|
+
export { isSafeResetStateDir } from "./server-helpers-config.js";
|
|
772
|
+
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
// Trade permission helpers (exported for use by awareness contributors)
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Resolve the active trade permission mode from config.
|
|
779
|
+
* Falls back to "user-sign-only" when not configured.
|
|
780
|
+
*/
|
|
781
|
+
export function resolveTradePermissionMode(
|
|
782
|
+
config: ElizaConfig,
|
|
783
|
+
): TradePermissionMode {
|
|
784
|
+
const raw = (config.features as Record<string, unknown> | undefined)
|
|
785
|
+
?.tradePermissionMode;
|
|
786
|
+
if (
|
|
787
|
+
raw === "user-sign-only" ||
|
|
788
|
+
raw === "manual-local-key" ||
|
|
789
|
+
raw === "agent-auto"
|
|
790
|
+
) {
|
|
791
|
+
return raw;
|
|
792
|
+
}
|
|
793
|
+
return "user-sign-only";
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Maximum number of autonomous agent trades allowed per calendar day.
|
|
798
|
+
* Acts as a safety rail when `agent-auto` mode is enabled.
|
|
799
|
+
*/
|
|
800
|
+
// Trade safety utilities (defined in trade-safety.ts for testability)
|
|
801
|
+
import {
|
|
802
|
+
canUseLocalTradeExecution,
|
|
803
|
+
type TradePermissionMode,
|
|
804
|
+
} from "./trade-safety.js";
|
|
805
|
+
|
|
806
|
+
export {
|
|
807
|
+
AGENT_AUTO_MAX_DAILY_TRADES,
|
|
808
|
+
agentAutoDailyTrades,
|
|
809
|
+
assertQuoteFresh,
|
|
810
|
+
canUseLocalTradeExecution,
|
|
811
|
+
getAgentAutoTradeDate,
|
|
812
|
+
QUOTE_MAX_AGE_MS,
|
|
813
|
+
recordAgentAutoTrade,
|
|
814
|
+
type TradePermissionMode,
|
|
815
|
+
} from "./trade-safety.js";
|
|
816
|
+
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// Automation & agent permission helpers
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
import type { AgentAutomationMode } from "./server-types.js";
|
|
822
|
+
|
|
823
|
+
const AGENT_AUTOMATION_HEADER = "x-eliza-agent-action";
|
|
824
|
+
const AGENT_AUTOMATION_MODES = new Set<AgentAutomationMode>([
|
|
825
|
+
"connectors-only",
|
|
826
|
+
"full",
|
|
827
|
+
]);
|
|
828
|
+
function parseAgentAutomationMode(value: unknown): AgentAutomationMode | null {
|
|
829
|
+
if (typeof value !== "string") return null;
|
|
830
|
+
const normalized = value.trim().toLowerCase();
|
|
831
|
+
if (!AGENT_AUTOMATION_MODES.has(normalized as AgentAutomationMode)) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
return normalized as AgentAutomationMode;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function _isAgentAutomationRequest(req: http.IncomingMessage): boolean {
|
|
838
|
+
const raw = req.headers[AGENT_AUTOMATION_HEADER];
|
|
839
|
+
if (typeof raw !== "string") return false;
|
|
840
|
+
return /^(1|true|yes|agent)$/i.test(raw.trim());
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function persistAgentAutomationMode(
|
|
844
|
+
state: ServerState,
|
|
845
|
+
mode: AgentAutomationMode,
|
|
846
|
+
): void {
|
|
847
|
+
state.agentAutomationMode = mode;
|
|
848
|
+
if (!state.config.features) {
|
|
849
|
+
state.config.features = {};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const features = state.config.features as Record<
|
|
853
|
+
string,
|
|
854
|
+
boolean | { enabled?: boolean; [k: string]: unknown }
|
|
855
|
+
>;
|
|
856
|
+
const current = features.agentAutomation;
|
|
857
|
+
const currentObject =
|
|
858
|
+
current && typeof current === "object" && !Array.isArray(current)
|
|
859
|
+
? (current as Record<string, unknown>)
|
|
860
|
+
: {};
|
|
861
|
+
|
|
862
|
+
features.agentAutomation = {
|
|
863
|
+
...currentObject,
|
|
864
|
+
enabled: true,
|
|
865
|
+
mode,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function buildPluginEvmDiagnosticEntry(
|
|
870
|
+
state: Pick<ServerState, "config" | "runtime">,
|
|
871
|
+
): PluginEntry {
|
|
872
|
+
const capability = resolveWalletCapabilityStatus(state);
|
|
873
|
+
const enabled =
|
|
874
|
+
capability.pluginEvmLoaded ||
|
|
875
|
+
capability.pluginEvmRequired ||
|
|
876
|
+
(state.config.plugins?.allow ?? []).some((entry) => {
|
|
877
|
+
return entry === EVM_PLUGIN_PACKAGE || entry === "evm";
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const capabilityStatus = capability.pluginEvmLoaded
|
|
881
|
+
? capability.pluginEvmRequired
|
|
882
|
+
? "loaded"
|
|
883
|
+
: "auto-enabled"
|
|
884
|
+
: enabled
|
|
885
|
+
? capability.evmAddress || capability.localSignerAvailable
|
|
886
|
+
? "blocked"
|
|
887
|
+
: "missing-prerequisites"
|
|
888
|
+
: "disabled";
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
id: "evm",
|
|
892
|
+
name: "Plugin EVM",
|
|
893
|
+
description:
|
|
894
|
+
"EVM wallet runtime for balance, transfer, and trade actions. Required for wallet execution in chat.",
|
|
895
|
+
tags: ["wallet", "evm", "bsc", "onchain"],
|
|
896
|
+
enabled,
|
|
897
|
+
configured: capability.pluginEvmRequired,
|
|
898
|
+
envKey: "EVM_PRIVATE_KEY",
|
|
899
|
+
category: "feature",
|
|
900
|
+
source: "bundled",
|
|
901
|
+
configKeys: [
|
|
902
|
+
"EVM_PRIVATE_KEY",
|
|
903
|
+
"BSC_RPC_URL",
|
|
904
|
+
"BSC_TESTNET_RPC_URL",
|
|
905
|
+
"ELIZA_WALLET_NETWORK",
|
|
906
|
+
],
|
|
907
|
+
parameters: [],
|
|
908
|
+
validationErrors: [],
|
|
909
|
+
validationWarnings: [],
|
|
910
|
+
npmName: EVM_PLUGIN_PACKAGE,
|
|
911
|
+
isActive: capability.pluginEvmLoaded,
|
|
912
|
+
autoEnabled: capability.pluginEvmRequired && !capability.pluginEvmLoaded,
|
|
913
|
+
managementMode: "core-optional",
|
|
914
|
+
capabilityStatus,
|
|
915
|
+
capabilityReason: capability.executionReady
|
|
916
|
+
? "Wallet execution is ready."
|
|
917
|
+
: capability.executionBlockedReason,
|
|
918
|
+
prerequisites: [
|
|
919
|
+
{ label: "wallet present", met: Boolean(capability.evmAddress) },
|
|
920
|
+
{ label: "rpc ready", met: capability.rpcReady },
|
|
921
|
+
{ label: "plugin loaded", met: capability.pluginEvmLoaded },
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Wallet intent/export helpers extracted to server-helpers-wallet.ts
|
|
927
|
+
import { resolveWalletExportRejection as _resolveWalletExportRejection } from "./server-helpers-wallet.js";
|
|
928
|
+
|
|
929
|
+
export {
|
|
930
|
+
hasUsableWalletFallbackParams,
|
|
931
|
+
inferWalletExecutionFallback,
|
|
932
|
+
resolveWalletExportRejection,
|
|
933
|
+
type WalletExportRejection,
|
|
934
|
+
} from "./server-helpers-wallet.js";
|
|
935
|
+
|
|
936
|
+
const resolveWalletExportRejection = _resolveWalletExportRejection;
|
|
937
|
+
|
|
938
|
+
// Plugin config helpers extracted to server-helpers-plugin.ts
|
|
939
|
+
import { resolvePluginConfigMutationRejections as _resolvePluginConfigMutationRejections } from "./server-helpers-plugin.js";
|
|
940
|
+
|
|
941
|
+
export {
|
|
942
|
+
type PluginConfigMutationRejection,
|
|
943
|
+
resolvePluginConfigMutationRejections,
|
|
944
|
+
resolvePluginConfigReply,
|
|
945
|
+
} from "./server-helpers-plugin.js";
|
|
946
|
+
|
|
947
|
+
const resolvePluginConfigMutationRejections =
|
|
948
|
+
_resolvePluginConfigMutationRejections;
|
|
949
|
+
|
|
950
|
+
// ---------------------------------------------------------------------------
|
|
951
|
+
// Route handler
|
|
952
|
+
// ---------------------------------------------------------------------------
|
|
953
|
+
|
|
954
|
+
interface RequestContext {
|
|
955
|
+
onRestart: (() => Promise<AgentRuntime | null>) | null;
|
|
956
|
+
onRuntimeSwapped?: () => void;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
import type { TrainingServiceWithRuntime } from "./server-types.js";
|
|
960
|
+
|
|
961
|
+
type TrainingServiceCtor = new (options: {
|
|
962
|
+
getRuntime: () => AgentRuntime | null;
|
|
963
|
+
getConfig: () => ElizaConfig;
|
|
964
|
+
setConfig: (nextConfig: ElizaConfig) => void;
|
|
965
|
+
}) => TrainingServiceWithRuntime;
|
|
966
|
+
|
|
967
|
+
async function resolveTrainingServiceCtor(): Promise<TrainingServiceCtor | null> {
|
|
968
|
+
const candidates = [
|
|
969
|
+
"../services/training-service",
|
|
970
|
+
"@elizaos/app-training",
|
|
971
|
+
"@elizaos/plugin-training",
|
|
972
|
+
] as const;
|
|
973
|
+
|
|
974
|
+
for (const specifier of candidates) {
|
|
975
|
+
try {
|
|
976
|
+
const loaded = (await import(/* @vite-ignore */ specifier)) as Record<
|
|
977
|
+
string,
|
|
978
|
+
unknown
|
|
979
|
+
>;
|
|
980
|
+
const ctor = loaded.TrainingService;
|
|
981
|
+
if (typeof ctor === "function") {
|
|
982
|
+
return ctor as TrainingServiceCtor;
|
|
983
|
+
}
|
|
984
|
+
} catch {
|
|
985
|
+
// Keep trying fallbacks.
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// mcpServersIncludeStdio, resolveMcpTerminalAuthorizationRejection extracted to server-helpers-mcp.ts
|
|
993
|
+
const resolveMcpTerminalAuthorizationRejection =
|
|
994
|
+
_resolveMcpTerminalAuthorizationRejection;
|
|
995
|
+
|
|
996
|
+
// Auth, CORS, pairing, terminal, WebSocket auth helpers extracted to server-helpers-auth.ts
|
|
997
|
+
import {
|
|
998
|
+
applyCors as _applyCors,
|
|
999
|
+
clearPairing as _clearPairing,
|
|
1000
|
+
ensureApiTokenForBindHost as _ensureApiTokenForBindHost,
|
|
1001
|
+
ensurePairingCode as _ensurePairingCode,
|
|
1002
|
+
getConfiguredApiToken as _getConfiguredApiToken,
|
|
1003
|
+
getPairingExpiresAt as _getPairingExpiresAt,
|
|
1004
|
+
isAllowedHost as _isAllowedHost,
|
|
1005
|
+
isAuthorized as _isAuthorized,
|
|
1006
|
+
isSharedTerminalClientId as _isSharedTerminalClientId,
|
|
1007
|
+
isWebSocketAuthorized as _isWebSocketAuthorized,
|
|
1008
|
+
normalizePairingCode as _normalizePairingCode,
|
|
1009
|
+
normalizeWsClientId as _normalizeWsClientId,
|
|
1010
|
+
pairingEnabled as _pairingEnabled,
|
|
1011
|
+
rateLimitPairing as _rateLimitPairing,
|
|
1012
|
+
rejectWebSocketUpgrade as _rejectWebSocketUpgrade,
|
|
1013
|
+
resolveTerminalRunClientId as _resolveTerminalRunClientId,
|
|
1014
|
+
resolveTerminalRunRejection as _resolveTerminalRunRejection,
|
|
1015
|
+
resolveWebSocketUpgradeRejection as _resolveWebSocketUpgradeRejection,
|
|
1016
|
+
} from "./server-helpers-auth.js";
|
|
1017
|
+
|
|
1018
|
+
export {
|
|
1019
|
+
ensureApiTokenForBindHost,
|
|
1020
|
+
extractAuthToken,
|
|
1021
|
+
isAllowedHost,
|
|
1022
|
+
isAuthorized,
|
|
1023
|
+
normalizeWsClientId,
|
|
1024
|
+
resolveCorsOrigin,
|
|
1025
|
+
resolveTerminalRunClientId,
|
|
1026
|
+
resolveTerminalRunRejection,
|
|
1027
|
+
resolveWebSocketUpgradeRejection,
|
|
1028
|
+
type TerminalRunRejection,
|
|
1029
|
+
type WebSocketUpgradeRejection,
|
|
1030
|
+
} from "./server-helpers-auth.js";
|
|
1031
|
+
|
|
1032
|
+
const isAllowedHost = _isAllowedHost;
|
|
1033
|
+
const applyCors = _applyCors;
|
|
1034
|
+
const isAuthorized = _isAuthorized;
|
|
1035
|
+
const ensureApiTokenForBindHost = _ensureApiTokenForBindHost;
|
|
1036
|
+
const normalizeWsClientId = _normalizeWsClientId;
|
|
1037
|
+
const resolveTerminalRunClientId = _resolveTerminalRunClientId;
|
|
1038
|
+
const isSharedTerminalClientId = _isSharedTerminalClientId;
|
|
1039
|
+
const resolveTerminalRunRejection = _resolveTerminalRunRejection;
|
|
1040
|
+
const resolveWebSocketUpgradeRejection = _resolveWebSocketUpgradeRejection;
|
|
1041
|
+
const rejectWebSocketUpgrade = _rejectWebSocketUpgrade;
|
|
1042
|
+
const isWebSocketAuthorized = _isWebSocketAuthorized;
|
|
1043
|
+
const getConfiguredApiToken = _getConfiguredApiToken;
|
|
1044
|
+
const pairingEnabled = _pairingEnabled;
|
|
1045
|
+
|
|
1046
|
+
function isLifeOpsCloudPluginRoute(pathname: string): boolean {
|
|
1047
|
+
return (
|
|
1048
|
+
pathname === "/api/cloud/features" ||
|
|
1049
|
+
pathname === "/api/cloud/features/sync" ||
|
|
1050
|
+
pathname.startsWith("/api/cloud/travel-providers/")
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
const ensurePairingCode = _ensurePairingCode;
|
|
1054
|
+
const normalizePairingCode = _normalizePairingCode;
|
|
1055
|
+
const rateLimitPairing = _rateLimitPairing;
|
|
1056
|
+
const getPairingExpiresAt = _getPairingExpiresAt;
|
|
1057
|
+
const clearPairing = _clearPairing;
|
|
1058
|
+
|
|
1059
|
+
/** Guard against concurrent provider switch requests (P0 §3). */
|
|
1060
|
+
let providerSwitchInProgress = false;
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Lazy per-process runtime operation manager. Constructed on first
|
|
1064
|
+
* request because it needs the per-server `state` reference + the
|
|
1065
|
+
* `onRestart` closure. Cached so subsequent requests see the same
|
|
1066
|
+
* active-op slot and execution chain.
|
|
1067
|
+
*/
|
|
1068
|
+
let cachedRuntimeOperationManager: RuntimeOperationManager | null = null;
|
|
1069
|
+
|
|
1070
|
+
function getOrCreateRuntimeOperationManager(
|
|
1071
|
+
state: ServerState,
|
|
1072
|
+
restartRuntime: (reason: string) => Promise<boolean>,
|
|
1073
|
+
): RuntimeOperationManager {
|
|
1074
|
+
if (cachedRuntimeOperationManager) {
|
|
1075
|
+
return cachedRuntimeOperationManager;
|
|
1076
|
+
}
|
|
1077
|
+
const repository = getDefaultRepository();
|
|
1078
|
+
const healthChecker = getDefaultHealthChecker();
|
|
1079
|
+
const coldStrategy = createColdStrategy({
|
|
1080
|
+
restartRuntime: async (reason) => {
|
|
1081
|
+
const ok = await restartRuntime(reason);
|
|
1082
|
+
if (!ok) return null;
|
|
1083
|
+
return state.runtime;
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
const hotStrategy = createHotStrategy({});
|
|
1087
|
+
const classifyContext = (): ClassifyContext => ({
|
|
1088
|
+
currentProvider: resolvePreferredProviderId(state.config),
|
|
1089
|
+
currentPrimaryModel: resolvePrimaryModel(state.config),
|
|
1090
|
+
});
|
|
1091
|
+
cachedRuntimeOperationManager = new DefaultRuntimeOperationManager({
|
|
1092
|
+
repository,
|
|
1093
|
+
runtime: () => state.runtime,
|
|
1094
|
+
classifyContext,
|
|
1095
|
+
classifier: defaultClassifier,
|
|
1096
|
+
healthChecker,
|
|
1097
|
+
strategies: { cold: coldStrategy, hot: hotStrategy },
|
|
1098
|
+
});
|
|
1099
|
+
return cachedRuntimeOperationManager;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// PluginConfigMutationRejection, resolvePluginConfigMutationRejections,
|
|
1103
|
+
// WalletExportRejection, resolveWalletExportRejection
|
|
1104
|
+
// extracted to server-helpers-plugin.ts and server-helpers-wallet.ts respectively.
|
|
1105
|
+
// Re-exported above.
|
|
1106
|
+
|
|
1107
|
+
// Terminal/WS/state-dir helpers extracted to server-helpers-auth.ts; re-exported above.
|
|
1108
|
+
|
|
1109
|
+
// decodePathComponent imported in the consolidated import at the top
|
|
1110
|
+
|
|
1111
|
+
// Workbench task/todo helpers — extracted to workbench-helpers.ts
|
|
1112
|
+
import {
|
|
1113
|
+
asObject,
|
|
1114
|
+
normalizeTags,
|
|
1115
|
+
parseNullableNumber,
|
|
1116
|
+
readTaskCompleted,
|
|
1117
|
+
readTaskMetadata,
|
|
1118
|
+
toWorkbenchTask,
|
|
1119
|
+
toWorkbenchTodo,
|
|
1120
|
+
WORKBENCH_TASK_TAG,
|
|
1121
|
+
WORKBENCH_TODO_TAG,
|
|
1122
|
+
} from "./workbench-helpers.js";
|
|
1123
|
+
|
|
1124
|
+
const _WORKBENCH_TASK_TAG = WORKBENCH_TASK_TAG;
|
|
1125
|
+
const _WORKBENCH_TODO_TAG = WORKBENCH_TODO_TAG;
|
|
1126
|
+
|
|
1127
|
+
// (workbench helpers moved to workbench-helpers.ts)
|
|
1128
|
+
|
|
1129
|
+
// ── Autonomy / swarm / coding-agent helpers — extracted to server-helpers-swarm.ts ──
|
|
1130
|
+
import {
|
|
1131
|
+
getCoordinatorFromRuntime,
|
|
1132
|
+
getPtyConsoleBridge,
|
|
1133
|
+
routeAutonomyTextToUser,
|
|
1134
|
+
wireCodingAgentBridgesNow,
|
|
1135
|
+
wireCodingAgentChatBridge,
|
|
1136
|
+
wireCodingAgentSwarmSynthesis,
|
|
1137
|
+
wireCodingAgentWsBridge,
|
|
1138
|
+
wireCoordinatorEventRouting,
|
|
1139
|
+
} from "./server-helpers-swarm.js";
|
|
1140
|
+
|
|
1141
|
+
export {
|
|
1142
|
+
handleSwarmSynthesis,
|
|
1143
|
+
routeAutonomyTextToUser,
|
|
1144
|
+
} from "./server-helpers-swarm.js";
|
|
1145
|
+
|
|
1146
|
+
async function maybeRouteAutonomyEventToConversation(
|
|
1147
|
+
state: ServerState,
|
|
1148
|
+
event: AgentEventPayloadLike,
|
|
1149
|
+
): Promise<void> {
|
|
1150
|
+
if (event.stream !== "assistant") return;
|
|
1151
|
+
|
|
1152
|
+
const payload =
|
|
1153
|
+
event.data && typeof event.data === "object"
|
|
1154
|
+
? (event.data as Record<string, unknown>)
|
|
1155
|
+
: null;
|
|
1156
|
+
const text = typeof payload?.text === "string" ? payload.text.trim() : "";
|
|
1157
|
+
if (!text) return;
|
|
1158
|
+
|
|
1159
|
+
const explicitSource =
|
|
1160
|
+
typeof payload?.source === "string" ? payload.source : null;
|
|
1161
|
+
const hasExplicitSource =
|
|
1162
|
+
explicitSource !== null && explicitSource.trim().length > 0;
|
|
1163
|
+
const source = hasExplicitSource ? explicitSource.trim() : "autonomy";
|
|
1164
|
+
|
|
1165
|
+
// Regular user conversation turns should never be re-routed as proactive.
|
|
1166
|
+
// Some AGENT_EVENT payloads may omit roomId metadata, so rely on source too.
|
|
1167
|
+
if (source === "client_chat") return;
|
|
1168
|
+
if (!hasExplicitSource && !event.roomId) return;
|
|
1169
|
+
|
|
1170
|
+
// Keep regular conversation messages in their own room only.
|
|
1171
|
+
if (
|
|
1172
|
+
event.roomId &&
|
|
1173
|
+
Array.from(state.conversations.values()).some(
|
|
1174
|
+
(c) => c.roomId === event.roomId,
|
|
1175
|
+
)
|
|
1176
|
+
) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
await routeAutonomyTextToUser(state, text, source);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async function handleRequest(
|
|
1184
|
+
req: http.IncomingMessage,
|
|
1185
|
+
res: http.ServerResponse,
|
|
1186
|
+
state: ServerState,
|
|
1187
|
+
ctx?: RequestContext,
|
|
1188
|
+
): Promise<void> {
|
|
1189
|
+
const method = req.method ?? "GET";
|
|
1190
|
+
let url: URL;
|
|
1191
|
+
try {
|
|
1192
|
+
url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1193
|
+
} catch {
|
|
1194
|
+
error(res, "Invalid request URL", 400);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const pathname = url.pathname;
|
|
1198
|
+
const isAuthEndpoint = pathname.startsWith("/api/auth/");
|
|
1199
|
+
const isHealthEndpoint = method === "GET" && pathname === "/api/health";
|
|
1200
|
+
const isCloudProvisioned = isCloudProvisionedContainer();
|
|
1201
|
+
const isCloudOnboardingStatusEndpoint =
|
|
1202
|
+
method === "GET" &&
|
|
1203
|
+
pathname === "/api/onboarding/status" &&
|
|
1204
|
+
isCloudProvisioned;
|
|
1205
|
+
const isWhatsAppWebhookEndpoint = pathname === "/api/whatsapp/webhook";
|
|
1206
|
+
const isBlueBubblesWebhookEndpoint =
|
|
1207
|
+
pathname ===
|
|
1208
|
+
resolveBlueBubblesWebhookPath({
|
|
1209
|
+
runtime: state.runtime
|
|
1210
|
+
? {
|
|
1211
|
+
getService: (type: string) =>
|
|
1212
|
+
(
|
|
1213
|
+
state.runtime as { getService: (t: string) => unknown }
|
|
1214
|
+
).getService(type),
|
|
1215
|
+
}
|
|
1216
|
+
: undefined,
|
|
1217
|
+
});
|
|
1218
|
+
const isAuthProtectedPath = isAuthProtectedRoute(pathname);
|
|
1219
|
+
const _registryService = state.registryService;
|
|
1220
|
+
|
|
1221
|
+
const canonicalizeRestartReason = (reason: string): string => {
|
|
1222
|
+
if (
|
|
1223
|
+
reason === "primary-changed" ||
|
|
1224
|
+
reason === "cloud-refreshed" ||
|
|
1225
|
+
reason === "Wallet configuration updated"
|
|
1226
|
+
) {
|
|
1227
|
+
return "Wallet configuration updated";
|
|
1228
|
+
}
|
|
1229
|
+
return reason;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const scheduleRuntimeRestart = (reason: string): void => {
|
|
1233
|
+
const canonicalReason = canonicalizeRestartReason(reason);
|
|
1234
|
+
if (state.pendingRestartReasons.length >= 50) {
|
|
1235
|
+
// Prevent unbounded growth — keep only first entry + latest
|
|
1236
|
+
state.pendingRestartReasons.splice(
|
|
1237
|
+
1,
|
|
1238
|
+
state.pendingRestartReasons.length - 1,
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (!state.pendingRestartReasons.includes(canonicalReason)) {
|
|
1242
|
+
state.pendingRestartReasons.push(canonicalReason);
|
|
1243
|
+
}
|
|
1244
|
+
logger.info(
|
|
1245
|
+
`[eliza-api] Restart required: ${canonicalReason} (${state.pendingRestartReasons.length} pending)`,
|
|
1246
|
+
);
|
|
1247
|
+
state.broadcastWs?.({
|
|
1248
|
+
type: "restart-required",
|
|
1249
|
+
reasons: [...state.pendingRestartReasons],
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const restartRuntime = async (reason: string): Promise<boolean> => {
|
|
1254
|
+
if (!ctx?.onRestart) {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
if (state.agentState === "restarting") {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const previousState = state.agentState;
|
|
1262
|
+
logger.info(`[eliza-api] Applying runtime reload: ${reason}`);
|
|
1263
|
+
state.agentState = "restarting";
|
|
1264
|
+
state.startup = { ...state.startup, phase: "restarting" };
|
|
1265
|
+
state.broadcastStatus?.();
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
const newRuntime = await ctx.onRestart();
|
|
1269
|
+
if (!newRuntime) {
|
|
1270
|
+
state.agentState = previousState;
|
|
1271
|
+
state.broadcastStatus?.();
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
state.runtime = newRuntime;
|
|
1276
|
+
state.chatConnectionReady = null;
|
|
1277
|
+
state.chatConnectionPromise = null;
|
|
1278
|
+
state.agentState = "running";
|
|
1279
|
+
state.agentName =
|
|
1280
|
+
newRuntime.character.name ?? resolveDefaultAgentName(state.config);
|
|
1281
|
+
state.model = detectRuntimeModel(newRuntime, state.config);
|
|
1282
|
+
state.startedAt = Date.now();
|
|
1283
|
+
state.pendingRestartReasons = [];
|
|
1284
|
+
ctx.onRuntimeSwapped?.();
|
|
1285
|
+
state.broadcastStatus?.();
|
|
1286
|
+
return true;
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
logger.warn(
|
|
1289
|
+
`[eliza-api] Runtime reload failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1290
|
+
);
|
|
1291
|
+
state.agentState = previousState;
|
|
1292
|
+
state.broadcastStatus?.();
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
// ── DNS rebinding protection ──────────────────────────────────────────
|
|
1298
|
+
// Reject requests whose Host header doesn't match a known loopback
|
|
1299
|
+
// hostname. Without this check an attacker can rebind their domain's
|
|
1300
|
+
// DNS to 127.0.0.1 and read the unauthenticated localhost API from a
|
|
1301
|
+
// malicious page.
|
|
1302
|
+
if (!isAllowedHost(req)) {
|
|
1303
|
+
const incomingHost = req.headers.host ?? "your-hostname";
|
|
1304
|
+
json(
|
|
1305
|
+
res,
|
|
1306
|
+
{
|
|
1307
|
+
error: "Forbidden — invalid Host header",
|
|
1308
|
+
hint: `To allow this host, set ELIZA_ALLOWED_HOSTS=${incomingHost} (or ELIZA_ALLOWED_HOSTS) in your environment, or access via http://localhost`,
|
|
1309
|
+
docs: "https://docs.eliza.ai/configuration#allowed-hosts",
|
|
1310
|
+
},
|
|
1311
|
+
403,
|
|
1312
|
+
);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (!applyCors(req, res, pathname)) {
|
|
1317
|
+
json(res, { error: "Origin not allowed" }, 403);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Serve dashboard static assets before the auth gates. serveStaticUi already
|
|
1322
|
+
// refuses /api/, /v1/, and /ws paths, so API endpoints remain protected
|
|
1323
|
+
// while steward-managed containers can still reach the built-in dashboard.
|
|
1324
|
+
if (method === "GET" || method === "HEAD") {
|
|
1325
|
+
if (serveStaticUi(req, res, pathname)) return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Single auth gate. The previous two-block arrangement (a cloud-provisioned
|
|
1329
|
+
// copy followed by an unconditional copy) was redundant: the unconditional
|
|
1330
|
+
// block already applied to cloud-provisioned requests because
|
|
1331
|
+
// `isAuthorized` consults `isCloudProvisionedContainer()` when no token is
|
|
1332
|
+
// configured.
|
|
1333
|
+
if (
|
|
1334
|
+
method !== "OPTIONS" &&
|
|
1335
|
+
isAuthProtectedPath &&
|
|
1336
|
+
!isAuthEndpoint &&
|
|
1337
|
+
!isHealthEndpoint &&
|
|
1338
|
+
!isCloudOnboardingStatusEndpoint &&
|
|
1339
|
+
!isWhatsAppWebhookEndpoint &&
|
|
1340
|
+
!isBlueBubblesWebhookEndpoint &&
|
|
1341
|
+
!isPublicRuntimePluginRoute({
|
|
1342
|
+
runtime: state.runtime,
|
|
1343
|
+
method,
|
|
1344
|
+
pathname,
|
|
1345
|
+
}) &&
|
|
1346
|
+
!isAuthorized(req)
|
|
1347
|
+
) {
|
|
1348
|
+
json(res, { error: "Unauthorized" }, 401);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// CORS preflight
|
|
1353
|
+
if (method === "OPTIONS") {
|
|
1354
|
+
res.statusCode = 204;
|
|
1355
|
+
res.end();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ── Provider inference helpers ────────────────────────────────────────
|
|
1360
|
+
const _disableCloudInference = (): void => {
|
|
1361
|
+
delete process.env.ANTHROPIC_BASE_URL;
|
|
1362
|
+
delete process.env.OPENAI_BASE_URL;
|
|
1363
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
1364
|
+
delete process.env.OPENAI_API_KEY;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const _enableCloudInference = (
|
|
1368
|
+
cloudApiKey: string,
|
|
1369
|
+
baseUrl: string,
|
|
1370
|
+
): void => {
|
|
1371
|
+
// Configure coding agent CLIs to proxy through ElizaCloud /api/v1
|
|
1372
|
+
process.env.ANTHROPIC_BASE_URL = `${baseUrl}/api/v1`;
|
|
1373
|
+
process.env.ANTHROPIC_API_KEY = cloudApiKey;
|
|
1374
|
+
process.env.OPENAI_BASE_URL = `${baseUrl}/api/v1`;
|
|
1375
|
+
process.env.OPENAI_API_KEY = cloudApiKey;
|
|
1376
|
+
// Gemini CLI and Aider — no proxy support via ElizaCloud inference
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// ── POST /api/provider/switch (extracted to provider-switch-routes.ts) ──
|
|
1380
|
+
const runtimeOperationManager = getOrCreateRuntimeOperationManager(
|
|
1381
|
+
state,
|
|
1382
|
+
restartRuntime,
|
|
1383
|
+
);
|
|
1384
|
+
if (
|
|
1385
|
+
await handleProviderSwitchRoutes({
|
|
1386
|
+
req,
|
|
1387
|
+
res,
|
|
1388
|
+
method,
|
|
1389
|
+
pathname,
|
|
1390
|
+
state,
|
|
1391
|
+
json,
|
|
1392
|
+
error,
|
|
1393
|
+
readJsonBody,
|
|
1394
|
+
saveElizaConfig,
|
|
1395
|
+
scheduleRuntimeRestart,
|
|
1396
|
+
providerSwitchInProgress,
|
|
1397
|
+
setProviderSwitchInProgress: (v: boolean) => {
|
|
1398
|
+
providerSwitchInProgress = v;
|
|
1399
|
+
},
|
|
1400
|
+
runtimeOperationManager,
|
|
1401
|
+
})
|
|
1402
|
+
) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if (
|
|
1407
|
+
await handleAuthRoutes({
|
|
1408
|
+
req,
|
|
1409
|
+
res,
|
|
1410
|
+
method,
|
|
1411
|
+
pathname,
|
|
1412
|
+
readJsonBody,
|
|
1413
|
+
json,
|
|
1414
|
+
error,
|
|
1415
|
+
pairingEnabled,
|
|
1416
|
+
ensurePairingCode,
|
|
1417
|
+
normalizePairingCode,
|
|
1418
|
+
rateLimitPairing,
|
|
1419
|
+
getPairingExpiresAt,
|
|
1420
|
+
clearPairing,
|
|
1421
|
+
})
|
|
1422
|
+
) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (
|
|
1427
|
+
await handleSubscriptionRoutes({
|
|
1428
|
+
req,
|
|
1429
|
+
res,
|
|
1430
|
+
method,
|
|
1431
|
+
pathname,
|
|
1432
|
+
state,
|
|
1433
|
+
readJsonBody,
|
|
1434
|
+
json,
|
|
1435
|
+
error,
|
|
1436
|
+
saveConfig: saveElizaConfig,
|
|
1437
|
+
loadSubscriptionAuth: async () =>
|
|
1438
|
+
(await import("../auth/index.js")) as never,
|
|
1439
|
+
} as never)
|
|
1440
|
+
) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (
|
|
1445
|
+
await handleAccountsRoutes({
|
|
1446
|
+
req,
|
|
1447
|
+
res,
|
|
1448
|
+
method,
|
|
1449
|
+
pathname,
|
|
1450
|
+
readJsonBody,
|
|
1451
|
+
json,
|
|
1452
|
+
error,
|
|
1453
|
+
state: { config: state.config },
|
|
1454
|
+
saveConfig: saveElizaConfig,
|
|
1455
|
+
})
|
|
1456
|
+
) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1461
|
+
// Health / status / runtime routes (extracted to health-routes.ts)
|
|
1462
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1463
|
+
if (
|
|
1464
|
+
await handleHealthRoutes({
|
|
1465
|
+
req,
|
|
1466
|
+
res,
|
|
1467
|
+
method,
|
|
1468
|
+
pathname,
|
|
1469
|
+
url,
|
|
1470
|
+
state,
|
|
1471
|
+
json,
|
|
1472
|
+
error,
|
|
1473
|
+
})
|
|
1474
|
+
) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// ── Onboarding GET routes (extracted to onboarding-routes.ts) ─────────
|
|
1479
|
+
if (
|
|
1480
|
+
await handleOnboardingRoutes({
|
|
1481
|
+
req,
|
|
1482
|
+
res,
|
|
1483
|
+
method,
|
|
1484
|
+
pathname,
|
|
1485
|
+
url,
|
|
1486
|
+
state: coerce<OnboardingRouteArg["state"]>(state),
|
|
1487
|
+
json,
|
|
1488
|
+
error,
|
|
1489
|
+
readJsonBody,
|
|
1490
|
+
isCloudProvisionedContainer,
|
|
1491
|
+
hasPersistedOnboardingState,
|
|
1492
|
+
ensureWalletKeysInEnvAndConfig,
|
|
1493
|
+
getWalletAddresses:
|
|
1494
|
+
coerce<OnboardingRouteArg["getWalletAddresses"]>(getWalletAddresses),
|
|
1495
|
+
pickRandomNames,
|
|
1496
|
+
getStylePresets:
|
|
1497
|
+
coerce<OnboardingRouteArg["getStylePresets"]>(getStylePresets),
|
|
1498
|
+
getProviderOptions:
|
|
1499
|
+
coerce<OnboardingRouteArg["getProviderOptions"]>(getProviderOptions),
|
|
1500
|
+
getCloudProviderOptions: coerce<
|
|
1501
|
+
OnboardingRouteArg["getCloudProviderOptions"]
|
|
1502
|
+
>(getCloudProviderOptions),
|
|
1503
|
+
getModelOptions:
|
|
1504
|
+
coerce<OnboardingRouteArg["getModelOptions"]>(getModelOptions),
|
|
1505
|
+
getInventoryProviderOptions: coerce<
|
|
1506
|
+
OnboardingRouteArg["getInventoryProviderOptions"]
|
|
1507
|
+
>(getInventoryProviderOptions),
|
|
1508
|
+
resolveConfiguredCharacterLanguage: coerce<
|
|
1509
|
+
OnboardingRouteArg["resolveConfiguredCharacterLanguage"]
|
|
1510
|
+
>(resolveConfiguredCharacterLanguage),
|
|
1511
|
+
normalizeCharacterLanguage: coerce<
|
|
1512
|
+
OnboardingRouteArg["normalizeCharacterLanguage"]
|
|
1513
|
+
>(normalizeCharacterLanguage),
|
|
1514
|
+
readUiLanguageHeader:
|
|
1515
|
+
coerce<OnboardingRouteArg["readUiLanguageHeader"]>(
|
|
1516
|
+
readUiLanguageHeader,
|
|
1517
|
+
),
|
|
1518
|
+
applyOnboardingVoicePreset: coerce<
|
|
1519
|
+
OnboardingRouteArg["applyOnboardingVoicePreset"]
|
|
1520
|
+
>(applyOnboardingVoicePreset),
|
|
1521
|
+
saveElizaConfig,
|
|
1522
|
+
})
|
|
1523
|
+
) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// POST /api/onboarding is now handled by onboarding-routes.ts above.
|
|
1528
|
+
|
|
1529
|
+
if (
|
|
1530
|
+
await handleAgentLifecycleRoutes({
|
|
1531
|
+
req,
|
|
1532
|
+
res,
|
|
1533
|
+
method,
|
|
1534
|
+
pathname,
|
|
1535
|
+
state,
|
|
1536
|
+
error,
|
|
1537
|
+
json,
|
|
1538
|
+
readJsonBody,
|
|
1539
|
+
})
|
|
1540
|
+
) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
const triggerHandled = await handleTriggerRoutes({
|
|
1545
|
+
req,
|
|
1546
|
+
res,
|
|
1547
|
+
method,
|
|
1548
|
+
pathname,
|
|
1549
|
+
runtime: state.runtime,
|
|
1550
|
+
readJsonBody,
|
|
1551
|
+
json,
|
|
1552
|
+
error,
|
|
1553
|
+
executeTriggerTask,
|
|
1554
|
+
getTriggerHealthSnapshot,
|
|
1555
|
+
getTriggerLimit,
|
|
1556
|
+
listTriggerTasks,
|
|
1557
|
+
readTriggerConfig,
|
|
1558
|
+
readTriggerRuns,
|
|
1559
|
+
taskToTriggerSummary,
|
|
1560
|
+
triggersFeatureEnabled,
|
|
1561
|
+
buildTriggerConfig,
|
|
1562
|
+
buildTriggerMetadata,
|
|
1563
|
+
normalizeTriggerDraft,
|
|
1564
|
+
DISABLED_TRIGGER_INTERVAL_MS,
|
|
1565
|
+
TRIGGER_TASK_NAME,
|
|
1566
|
+
TRIGGER_TASK_TAGS: [...TRIGGER_TASK_TAGS],
|
|
1567
|
+
});
|
|
1568
|
+
if (triggerHandled) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (pathname.startsWith("/api/training")) {
|
|
1573
|
+
if (!state.trainingService) {
|
|
1574
|
+
error(res, "Training service is not available", 503);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const trainingHandled = await handleTrainingRoutes({
|
|
1578
|
+
req,
|
|
1579
|
+
res,
|
|
1580
|
+
method,
|
|
1581
|
+
pathname,
|
|
1582
|
+
runtime: state.runtime,
|
|
1583
|
+
trainingService: state.trainingService,
|
|
1584
|
+
readJsonBody,
|
|
1585
|
+
json,
|
|
1586
|
+
error,
|
|
1587
|
+
isLoopbackHost,
|
|
1588
|
+
});
|
|
1589
|
+
if (trainingHandled) return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// ── Knowledge routes (/api/knowledge/*) ─────────────────────────────────
|
|
1593
|
+
if (pathname.startsWith("/api/knowledge")) {
|
|
1594
|
+
const knowledgeHandled = await handleKnowledgeRoutes({
|
|
1595
|
+
req,
|
|
1596
|
+
res,
|
|
1597
|
+
method,
|
|
1598
|
+
pathname,
|
|
1599
|
+
url,
|
|
1600
|
+
runtime: state.runtime,
|
|
1601
|
+
readJsonBody,
|
|
1602
|
+
json,
|
|
1603
|
+
error,
|
|
1604
|
+
});
|
|
1605
|
+
if (knowledgeHandled) return;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (
|
|
1609
|
+
pathname.startsWith("/api/memory") ||
|
|
1610
|
+
pathname.startsWith("/api/memories") ||
|
|
1611
|
+
pathname === "/api/context/quick"
|
|
1612
|
+
) {
|
|
1613
|
+
const memoryHandled = await handleMemoryRoutes({
|
|
1614
|
+
req,
|
|
1615
|
+
res,
|
|
1616
|
+
method,
|
|
1617
|
+
pathname,
|
|
1618
|
+
url,
|
|
1619
|
+
runtime: state.runtime,
|
|
1620
|
+
agentName: state.agentName,
|
|
1621
|
+
readJsonBody,
|
|
1622
|
+
json,
|
|
1623
|
+
error,
|
|
1624
|
+
});
|
|
1625
|
+
if (memoryHandled) return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (
|
|
1629
|
+
await handleAgentAdminRoutes({
|
|
1630
|
+
req,
|
|
1631
|
+
res,
|
|
1632
|
+
method,
|
|
1633
|
+
pathname,
|
|
1634
|
+
state,
|
|
1635
|
+
onRestart: ctx?.onRestart ?? undefined,
|
|
1636
|
+
onRuntimeSwapped: ctx?.onRuntimeSwapped,
|
|
1637
|
+
json,
|
|
1638
|
+
error,
|
|
1639
|
+
resolveStateDir,
|
|
1640
|
+
resolvePath: path.resolve,
|
|
1641
|
+
getHomeDir: os.homedir,
|
|
1642
|
+
isSafeResetStateDir,
|
|
1643
|
+
stateDirExists: fs.existsSync,
|
|
1644
|
+
removeStateDir: (resolvedState) => {
|
|
1645
|
+
fs.rmSync(resolvedState, { recursive: true, force: true });
|
|
1646
|
+
},
|
|
1647
|
+
logWarn: (message) => logger.warn(message),
|
|
1648
|
+
})
|
|
1649
|
+
) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (
|
|
1654
|
+
await handleAgentTransferRoutes({
|
|
1655
|
+
req,
|
|
1656
|
+
res,
|
|
1657
|
+
method,
|
|
1658
|
+
pathname,
|
|
1659
|
+
state,
|
|
1660
|
+
readJsonBody,
|
|
1661
|
+
json,
|
|
1662
|
+
error,
|
|
1663
|
+
exportAgent,
|
|
1664
|
+
estimateExportSize,
|
|
1665
|
+
importAgent,
|
|
1666
|
+
isAgentExportError: (err: unknown) => err instanceof AgentExportError,
|
|
1667
|
+
})
|
|
1668
|
+
) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (
|
|
1673
|
+
await handleCharacterRoutes({
|
|
1674
|
+
req,
|
|
1675
|
+
res,
|
|
1676
|
+
method,
|
|
1677
|
+
pathname,
|
|
1678
|
+
state,
|
|
1679
|
+
readJsonBody,
|
|
1680
|
+
json,
|
|
1681
|
+
error,
|
|
1682
|
+
pickRandomNames,
|
|
1683
|
+
saveConfig: saveElizaConfig as never,
|
|
1684
|
+
validateCharacter: (body) => CharacterSchema.safeParse(body) as never,
|
|
1685
|
+
})
|
|
1686
|
+
) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (
|
|
1691
|
+
await handleExperienceRoutes({
|
|
1692
|
+
req,
|
|
1693
|
+
res,
|
|
1694
|
+
method,
|
|
1695
|
+
pathname,
|
|
1696
|
+
runtime: state.runtime,
|
|
1697
|
+
url,
|
|
1698
|
+
readJsonBody,
|
|
1699
|
+
json,
|
|
1700
|
+
error,
|
|
1701
|
+
})
|
|
1702
|
+
) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Compatibility route used by legacy health probes and desktop name lookup.
|
|
1707
|
+
if (method === "GET" && pathname === "/api/agents") {
|
|
1708
|
+
const runtimeAgentId =
|
|
1709
|
+
typeof state.runtime?.agentId === "string" &&
|
|
1710
|
+
state.runtime.agentId.trim().length > 0
|
|
1711
|
+
? state.runtime.agentId.trim()
|
|
1712
|
+
: null;
|
|
1713
|
+
const configuredAgentId =
|
|
1714
|
+
typeof state.config.agents?.list?.[0]?.id === "string" &&
|
|
1715
|
+
state.config.agents.list[0].id.trim().length > 0
|
|
1716
|
+
? state.config.agents.list[0].id.trim()
|
|
1717
|
+
: null;
|
|
1718
|
+
const agentName =
|
|
1719
|
+
state.runtime?.character.name?.trim() ||
|
|
1720
|
+
state.agentName?.trim() ||
|
|
1721
|
+
"Eliza";
|
|
1722
|
+
|
|
1723
|
+
json(res, {
|
|
1724
|
+
agents: [
|
|
1725
|
+
{
|
|
1726
|
+
id:
|
|
1727
|
+
runtimeAgentId ??
|
|
1728
|
+
configuredAgentId ??
|
|
1729
|
+
"00000000-0000-0000-0000-000000000000",
|
|
1730
|
+
name: agentName,
|
|
1731
|
+
status: state.agentState,
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
});
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (
|
|
1739
|
+
await handleModelsRoutes({
|
|
1740
|
+
req,
|
|
1741
|
+
res,
|
|
1742
|
+
method,
|
|
1743
|
+
pathname,
|
|
1744
|
+
url,
|
|
1745
|
+
json,
|
|
1746
|
+
providerCachePath,
|
|
1747
|
+
getOrFetchProvider,
|
|
1748
|
+
getOrFetchAllProviders,
|
|
1749
|
+
resolveModelsCacheDir,
|
|
1750
|
+
pathExists: fs.existsSync,
|
|
1751
|
+
readDir: fs.readdirSync,
|
|
1752
|
+
unlinkFile: fs.unlinkSync,
|
|
1753
|
+
joinPath: path.join,
|
|
1754
|
+
})
|
|
1755
|
+
) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// ── NFA routes (/api/nfa/*) ─────────────────────────────────────────
|
|
1760
|
+
// Extracted — will move to @elizaos/plugin-bnb-identity (Plugin.routes)
|
|
1761
|
+
// when the plugin directory is created. Until then, NFA routes are
|
|
1762
|
+
// served inline from nfa-routes.ts if needed, or disabled.
|
|
1763
|
+
|
|
1764
|
+
if (
|
|
1765
|
+
await handleRegistryRoutes({
|
|
1766
|
+
req,
|
|
1767
|
+
res,
|
|
1768
|
+
method,
|
|
1769
|
+
pathname,
|
|
1770
|
+
url,
|
|
1771
|
+
json,
|
|
1772
|
+
error,
|
|
1773
|
+
getPluginManager: () => getPluginManagerForState(state) as never,
|
|
1774
|
+
getLoadedPluginNames: () =>
|
|
1775
|
+
state.runtime?.plugins.map((plugin) => plugin.name) ?? [],
|
|
1776
|
+
getBundledPluginIds: () => getReleaseBundledPluginIds(),
|
|
1777
|
+
classifyRegistryPluginRelease,
|
|
1778
|
+
})
|
|
1779
|
+
) {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1784
|
+
// Plugin routes (extracted to plugin-routes.ts)
|
|
1785
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1786
|
+
if (
|
|
1787
|
+
pathname === "/api/plugins" ||
|
|
1788
|
+
pathname.startsWith("/api/plugins/") ||
|
|
1789
|
+
pathname === "/api/secrets" ||
|
|
1790
|
+
pathname === "/api/core/status" ||
|
|
1791
|
+
pathname === "/api/integrations/tavily-key"
|
|
1792
|
+
) {
|
|
1793
|
+
if (
|
|
1794
|
+
await handlePluginRoutes({
|
|
1795
|
+
req,
|
|
1796
|
+
res,
|
|
1797
|
+
method,
|
|
1798
|
+
pathname,
|
|
1799
|
+
url,
|
|
1800
|
+
state,
|
|
1801
|
+
json,
|
|
1802
|
+
error,
|
|
1803
|
+
readJsonBody,
|
|
1804
|
+
scheduleRuntimeRestart,
|
|
1805
|
+
restartRuntime,
|
|
1806
|
+
BLOCKED_ENV_KEYS,
|
|
1807
|
+
discoverInstalledPlugins,
|
|
1808
|
+
maskValue,
|
|
1809
|
+
aggregateSecrets,
|
|
1810
|
+
readProviderCache,
|
|
1811
|
+
paramKeyToCategory,
|
|
1812
|
+
buildPluginEvmDiagnosticEntry,
|
|
1813
|
+
EVM_PLUGIN_PACKAGE,
|
|
1814
|
+
applyWhatsAppQrOverride,
|
|
1815
|
+
applySignalQrOverride,
|
|
1816
|
+
signalAuthExists,
|
|
1817
|
+
resolvePluginConfigMutationRejections,
|
|
1818
|
+
requirePluginManager,
|
|
1819
|
+
requireCoreManager,
|
|
1820
|
+
})
|
|
1821
|
+
) {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1827
|
+
// Skills routes (extracted to skills-routes.ts)
|
|
1828
|
+
// Curated-skills routes live at /api/skills/curated/* and must be dispatched
|
|
1829
|
+
// before the generic skills routes (which reject "/" in skill IDs).
|
|
1830
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1831
|
+
if (pathname.startsWith("/api/skills/curated")) {
|
|
1832
|
+
if (
|
|
1833
|
+
await handleCuratedSkillsRoutes({
|
|
1834
|
+
req,
|
|
1835
|
+
res,
|
|
1836
|
+
method,
|
|
1837
|
+
pathname,
|
|
1838
|
+
url,
|
|
1839
|
+
json,
|
|
1840
|
+
error,
|
|
1841
|
+
readJsonBody,
|
|
1842
|
+
})
|
|
1843
|
+
) {
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (pathname.startsWith("/api/skills")) {
|
|
1848
|
+
if (
|
|
1849
|
+
await handleSkillsRoutes({
|
|
1850
|
+
req,
|
|
1851
|
+
res,
|
|
1852
|
+
method,
|
|
1853
|
+
pathname,
|
|
1854
|
+
url,
|
|
1855
|
+
state,
|
|
1856
|
+
json,
|
|
1857
|
+
error,
|
|
1858
|
+
readJsonBody,
|
|
1859
|
+
readBody,
|
|
1860
|
+
discoverSkills,
|
|
1861
|
+
saveElizaConfig,
|
|
1862
|
+
})
|
|
1863
|
+
) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (
|
|
1869
|
+
await handleDiagnosticsRoutes({
|
|
1870
|
+
req,
|
|
1871
|
+
res,
|
|
1872
|
+
method,
|
|
1873
|
+
pathname,
|
|
1874
|
+
url,
|
|
1875
|
+
logBuffer: state.logBuffer,
|
|
1876
|
+
clearLogBuffer: () => {
|
|
1877
|
+
const previous = state.logBuffer.length;
|
|
1878
|
+
state.logBuffer.length = 0;
|
|
1879
|
+
return previous;
|
|
1880
|
+
},
|
|
1881
|
+
readJsonBody,
|
|
1882
|
+
error,
|
|
1883
|
+
eventBuffer: state.eventBuffer,
|
|
1884
|
+
initSse: initSseFromChatRoutes,
|
|
1885
|
+
writeSseJson: writeSseJsonFromChatRoutes,
|
|
1886
|
+
json,
|
|
1887
|
+
auditEventTypes: AUDIT_EVENT_TYPES,
|
|
1888
|
+
auditSeverities: AUDIT_SEVERITIES,
|
|
1889
|
+
getAuditFeedSize,
|
|
1890
|
+
queryAuditFeed: (query) =>
|
|
1891
|
+
queryAuditFeed({
|
|
1892
|
+
type: (AUDIT_EVENT_TYPES as readonly string[]).includes(
|
|
1893
|
+
query.type ?? "",
|
|
1894
|
+
)
|
|
1895
|
+
? (query.type as (typeof AUDIT_EVENT_TYPES)[number])
|
|
1896
|
+
: undefined,
|
|
1897
|
+
severity: (AUDIT_SEVERITIES as readonly string[]).includes(
|
|
1898
|
+
query.severity ?? "",
|
|
1899
|
+
)
|
|
1900
|
+
? (query.severity as (typeof AUDIT_SEVERITIES)[number])
|
|
1901
|
+
: undefined,
|
|
1902
|
+
sinceMs: query.sinceMs,
|
|
1903
|
+
limit: query.limit,
|
|
1904
|
+
}).map((entry) => ({
|
|
1905
|
+
timestamp: entry.timestamp,
|
|
1906
|
+
type: entry.type,
|
|
1907
|
+
summary: entry.summary,
|
|
1908
|
+
severity: entry.severity,
|
|
1909
|
+
metadata: entry.metadata,
|
|
1910
|
+
})),
|
|
1911
|
+
subscribeAuditFeed,
|
|
1912
|
+
})
|
|
1913
|
+
) {
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1918
|
+
// Bug report routes
|
|
1919
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1920
|
+
if (
|
|
1921
|
+
await handleBugReportRoutes({
|
|
1922
|
+
req,
|
|
1923
|
+
res,
|
|
1924
|
+
method,
|
|
1925
|
+
pathname,
|
|
1926
|
+
readJsonBody,
|
|
1927
|
+
json,
|
|
1928
|
+
error,
|
|
1929
|
+
})
|
|
1930
|
+
) {
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1935
|
+
// Wallet core routes (addresses, balances, generate, config, export)
|
|
1936
|
+
// Canonical implementation lives in @elizaos/app-steward; wired here
|
|
1937
|
+
// so the API server exposes them without requiring plugin registration.
|
|
1938
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1939
|
+
if (pathname.startsWith("/api/wallet/")) {
|
|
1940
|
+
let stewardWalletCoreRoutes:
|
|
1941
|
+
| ((
|
|
1942
|
+
req: http.IncomingMessage,
|
|
1943
|
+
res: http.ServerResponse,
|
|
1944
|
+
state: unknown,
|
|
1945
|
+
) => Promise<boolean>)
|
|
1946
|
+
| null = null;
|
|
1947
|
+
try {
|
|
1948
|
+
const { handleWalletCoreRoutes } = await import(
|
|
1949
|
+
"@elizaos/app-steward/routes/wallet-core-routes"
|
|
1950
|
+
);
|
|
1951
|
+
stewardWalletCoreRoutes = handleWalletCoreRoutes;
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
if (isWalletBridgeImportFailure(err)) {
|
|
1954
|
+
logger.debug(
|
|
1955
|
+
{ err },
|
|
1956
|
+
"[eliza-api] Wallet core routes unavailable from @elizaos/app-steward; falling back to local bridge",
|
|
1957
|
+
);
|
|
1958
|
+
} else {
|
|
1959
|
+
logger.error({ err }, "[eliza-api] Wallet core route bridge failed");
|
|
1960
|
+
error(res, getErrorMessage(err), 500);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (stewardWalletCoreRoutes) {
|
|
1965
|
+
try {
|
|
1966
|
+
if (
|
|
1967
|
+
await stewardWalletCoreRoutes(req, res, {
|
|
1968
|
+
runtime: state.runtime ?? null,
|
|
1969
|
+
restartRuntime,
|
|
1970
|
+
scheduleRuntimeRestart,
|
|
1971
|
+
})
|
|
1972
|
+
) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
logger.error({ err }, "[eliza-api] Wallet core route bridge failed");
|
|
1977
|
+
error(res, getErrorMessage(err), 500);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (
|
|
1982
|
+
await handleWalletRoutes({
|
|
1983
|
+
req,
|
|
1984
|
+
res,
|
|
1985
|
+
method,
|
|
1986
|
+
pathname,
|
|
1987
|
+
config: loadElizaConfig(),
|
|
1988
|
+
saveConfig: saveElizaConfig,
|
|
1989
|
+
ensureWalletKeysInEnvAndConfig,
|
|
1990
|
+
resolveWalletExportRejection,
|
|
1991
|
+
restartRuntime,
|
|
1992
|
+
scheduleRuntimeRestart,
|
|
1993
|
+
readJsonBody,
|
|
1994
|
+
json,
|
|
1995
|
+
error,
|
|
1996
|
+
runtime: state.runtime ?? null,
|
|
1997
|
+
})
|
|
1998
|
+
) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2004
|
+
// ERC-8004 Registry, Agent self-status, Privy — delegated to agent-status-routes.ts
|
|
2005
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2006
|
+
if (
|
|
2007
|
+
await handleAgentStatusRoutes({
|
|
2008
|
+
req,
|
|
2009
|
+
res,
|
|
2010
|
+
method,
|
|
2011
|
+
pathname,
|
|
2012
|
+
url,
|
|
2013
|
+
state: coerce<AgentStatusRouteArg["state"]>(state),
|
|
2014
|
+
json,
|
|
2015
|
+
error,
|
|
2016
|
+
readJsonBody,
|
|
2017
|
+
deps: {
|
|
2018
|
+
getWalletAddresses,
|
|
2019
|
+
resolveWalletCapabilityStatus: coerce<
|
|
2020
|
+
AgentStatusRouteArg["deps"]["resolveWalletCapabilityStatus"]
|
|
2021
|
+
>(resolveWalletCapabilityStatus),
|
|
2022
|
+
resolveWalletRpcReadiness: coerce<
|
|
2023
|
+
AgentStatusRouteArg["deps"]["resolveWalletRpcReadiness"]
|
|
2024
|
+
>(resolveWalletRpcReadiness),
|
|
2025
|
+
resolveTradePermissionMode,
|
|
2026
|
+
canUseLocalTradeExecution: coerce<
|
|
2027
|
+
AgentStatusRouteArg["deps"]["canUseLocalTradeExecution"]
|
|
2028
|
+
>(canUseLocalTradeExecution),
|
|
2029
|
+
detectRuntimeModel:
|
|
2030
|
+
coerce<AgentStatusRouteArg["deps"]["detectRuntimeModel"]>(
|
|
2031
|
+
detectRuntimeModel,
|
|
2032
|
+
),
|
|
2033
|
+
resolveProviderFromModel,
|
|
2034
|
+
getGlobalAwarenessRegistry: coerce<
|
|
2035
|
+
AgentStatusRouteArg["deps"]["getGlobalAwarenessRegistry"]
|
|
2036
|
+
>(getGlobalAwarenessRegistry),
|
|
2037
|
+
isPrivyWalletProvisioningEnabled,
|
|
2038
|
+
ensurePrivyWalletsForCustomUser: coerce<
|
|
2039
|
+
AgentStatusRouteArg["deps"]["ensurePrivyWalletsForCustomUser"]
|
|
2040
|
+
>(ensurePrivyWalletsForCustomUser),
|
|
2041
|
+
RegistryService,
|
|
2042
|
+
},
|
|
2043
|
+
})
|
|
2044
|
+
) {
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// ── Update routes (extracted to update-routes.ts) ─────────────────────
|
|
2049
|
+
if (
|
|
2050
|
+
await handleUpdateRoutes({
|
|
2051
|
+
req,
|
|
2052
|
+
res,
|
|
2053
|
+
method,
|
|
2054
|
+
pathname,
|
|
2055
|
+
url,
|
|
2056
|
+
state,
|
|
2057
|
+
json,
|
|
2058
|
+
error,
|
|
2059
|
+
readJsonBody,
|
|
2060
|
+
saveElizaConfig,
|
|
2061
|
+
})
|
|
2062
|
+
) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// ── Connector routes (extracted to connector-routes.ts) ──────────────
|
|
2067
|
+
if (
|
|
2068
|
+
await handleConnectorRoutes({
|
|
2069
|
+
req,
|
|
2070
|
+
res,
|
|
2071
|
+
method,
|
|
2072
|
+
pathname,
|
|
2073
|
+
state,
|
|
2074
|
+
json,
|
|
2075
|
+
error,
|
|
2076
|
+
readJsonBody,
|
|
2077
|
+
saveElizaConfig,
|
|
2078
|
+
redactConfigSecrets,
|
|
2079
|
+
isBlockedObjectKey,
|
|
2080
|
+
cloneWithoutBlockedObjectKeys,
|
|
2081
|
+
onConnectorDisconnect: async (connectorName) => {
|
|
2082
|
+
// Disconnect cascades to the n8n credential cache: without this,
|
|
2083
|
+
// credStore.get() returns a stale n8n credential id and the next
|
|
2084
|
+
// workflow generation silently bypasses the missing-credentials
|
|
2085
|
+
// banner.
|
|
2086
|
+
const credTypes = credTypesForConnector(connectorName);
|
|
2087
|
+
if (credTypes.length === 0) return;
|
|
2088
|
+
const runtime = state.runtime;
|
|
2089
|
+
if (!runtime) return;
|
|
2090
|
+
const credStore = runtime.getService("n8n_credential_store") as
|
|
2091
|
+
| {
|
|
2092
|
+
delete?: (userId: string, credType: string) => Promise<void>;
|
|
2093
|
+
}
|
|
2094
|
+
| null;
|
|
2095
|
+
if (!credStore?.delete) return;
|
|
2096
|
+
const userId = runtime.agentId;
|
|
2097
|
+
await Promise.all(
|
|
2098
|
+
credTypes.map((credType) =>
|
|
2099
|
+
credStore.delete!(userId, credType).catch(() => {
|
|
2100
|
+
/* per-credType failure shouldn't block siblings */
|
|
2101
|
+
}),
|
|
2102
|
+
),
|
|
2103
|
+
);
|
|
2104
|
+
},
|
|
2105
|
+
})
|
|
2106
|
+
) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// ── WhatsApp routes (/api/whatsapp/*) ────────────────────────────────────
|
|
2111
|
+
// Moved to @elizaos/plugin-whatsapp setup-routes.ts (registered via Plugin.routes).
|
|
2112
|
+
|
|
2113
|
+
// ── Inbox routes (/api/inbox/*) ───────────────────────────────
|
|
2114
|
+
// Cross-channel read-only feed that merges connector messages
|
|
2115
|
+
// (imessage, telegram, discord, whatsapp, etc.) into a single
|
|
2116
|
+
// time-ordered view. See api/inbox-routes.ts for details.
|
|
2117
|
+
const blueBubblesHandled = await handleBlueBubblesRoute(
|
|
2118
|
+
req,
|
|
2119
|
+
res,
|
|
2120
|
+
pathname,
|
|
2121
|
+
method,
|
|
2122
|
+
{
|
|
2123
|
+
runtime: state.runtime
|
|
2124
|
+
? {
|
|
2125
|
+
getService: (type: string) =>
|
|
2126
|
+
(
|
|
2127
|
+
state.runtime as { getService: (t: string) => unknown }
|
|
2128
|
+
).getService(type),
|
|
2129
|
+
}
|
|
2130
|
+
: undefined,
|
|
2131
|
+
},
|
|
2132
|
+
{ json, error, readJsonBody },
|
|
2133
|
+
);
|
|
2134
|
+
if (blueBubblesHandled) return;
|
|
2135
|
+
|
|
2136
|
+
if (pathname.startsWith("/api/inbox")) {
|
|
2137
|
+
const handled = await handleInboxRoute(
|
|
2138
|
+
req,
|
|
2139
|
+
res,
|
|
2140
|
+
pathname,
|
|
2141
|
+
method,
|
|
2142
|
+
{ runtime: state.runtime ?? null },
|
|
2143
|
+
{ json, error, readJsonBody },
|
|
2144
|
+
);
|
|
2145
|
+
if (handled) return;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// ── iMessage routes (/api/imessage/*) ─────────────────────────────────
|
|
2149
|
+
// Extracted to @elizaos/plugin-imessage setup-routes.ts (Plugin.routes).
|
|
2150
|
+
// The plugin registers rawPath routes that serve the same legacy paths.
|
|
2151
|
+
|
|
2152
|
+
// ── Cloud relay status (/api/cloud/relay-status) ──────────────────────
|
|
2153
|
+
if (pathname === "/api/cloud/relay-status") {
|
|
2154
|
+
const handled = await handleCloudRelayRoute(
|
|
2155
|
+
req,
|
|
2156
|
+
res,
|
|
2157
|
+
pathname,
|
|
2158
|
+
method,
|
|
2159
|
+
{
|
|
2160
|
+
runtime: state.runtime
|
|
2161
|
+
? {
|
|
2162
|
+
getService: (type: string) =>
|
|
2163
|
+
(
|
|
2164
|
+
state.runtime as { getService: (t: string) => unknown }
|
|
2165
|
+
).getService(type),
|
|
2166
|
+
}
|
|
2167
|
+
: undefined,
|
|
2168
|
+
},
|
|
2169
|
+
{ json, error, readJsonBody },
|
|
2170
|
+
);
|
|
2171
|
+
if (handled) return;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// Telegram setup routes: now handled by @elizaos/plugin-telegram via
|
|
2175
|
+
// runtime plugin routes (rawPath: true). See plugin-telegram/src/setup-routes.ts.
|
|
2176
|
+
|
|
2177
|
+
// ── Telegram account routes (/api/telegram-account/*) ────────────────
|
|
2178
|
+
if (pathname.startsWith("/api/telegram-account")) {
|
|
2179
|
+
const routeState = {
|
|
2180
|
+
config: state.config,
|
|
2181
|
+
saveConfig: () => saveElizaConfig(state.config),
|
|
2182
|
+
runtime: state.runtime
|
|
2183
|
+
? {
|
|
2184
|
+
getService: (type: string) =>
|
|
2185
|
+
(
|
|
2186
|
+
state.runtime as { getService: (t: string) => unknown }
|
|
2187
|
+
).getService(type),
|
|
2188
|
+
getSetting: (key: string) =>
|
|
2189
|
+
(
|
|
2190
|
+
state.runtime as {
|
|
2191
|
+
getSetting: (k: string) => string | undefined;
|
|
2192
|
+
}
|
|
2193
|
+
).getSetting(key),
|
|
2194
|
+
}
|
|
2195
|
+
: undefined,
|
|
2196
|
+
telegramAccountAuthSession: state.telegramAccountAuthSession,
|
|
2197
|
+
};
|
|
2198
|
+
const handled = await handleTelegramAccountRoute(
|
|
2199
|
+
req,
|
|
2200
|
+
res,
|
|
2201
|
+
pathname,
|
|
2202
|
+
method,
|
|
2203
|
+
routeState,
|
|
2204
|
+
{ json, error, readJsonBody },
|
|
2205
|
+
{
|
|
2206
|
+
createAuthSession: (options) => new TelegramAccountAuthSession(options),
|
|
2207
|
+
authStateExists: telegramAccountAuthStateExists,
|
|
2208
|
+
sessionExists: telegramAccountSessionExists,
|
|
2209
|
+
clearAuthState: clearTelegramAccountAuthState,
|
|
2210
|
+
clearSession: clearTelegramAccountSession,
|
|
2211
|
+
},
|
|
2212
|
+
);
|
|
2213
|
+
state.telegramAccountAuthSession =
|
|
2214
|
+
routeState.telegramAccountAuthSession ?? null;
|
|
2215
|
+
if (handled) return;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// ── Discord Local routes (/api/discord-local/*) — extracted to @elizaos/plugin-discord (setup-routes.ts) ──
|
|
2219
|
+
|
|
2220
|
+
// ── Signal routes (/api/signal/*) — extracted to @elizaos/plugin-signal (setup-routes.ts) ──
|
|
2221
|
+
|
|
2222
|
+
// ── Restart ──────────────────────────────────────────────────────────
|
|
2223
|
+
if (method === "POST" && pathname === "/api/restart") {
|
|
2224
|
+
state.agentState = "restarting";
|
|
2225
|
+
state.startup = { ...state.startup, phase: "restarting" };
|
|
2226
|
+
state.broadcastStatus?.();
|
|
2227
|
+
json(res, { ok: true, message: "Restarting...", restarting: true });
|
|
2228
|
+
setTimeout(() => process.exit(0), 1000);
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// ── TTS routes (extracted to tts-routes.ts) ──────────────────────────
|
|
2233
|
+
if (
|
|
2234
|
+
await handleTtsRoutes({
|
|
2235
|
+
req,
|
|
2236
|
+
res,
|
|
2237
|
+
method,
|
|
2238
|
+
pathname,
|
|
2239
|
+
state,
|
|
2240
|
+
json,
|
|
2241
|
+
error,
|
|
2242
|
+
readJsonBody,
|
|
2243
|
+
isRedactedSecretValue,
|
|
2244
|
+
fetchWithTimeoutGuard,
|
|
2245
|
+
streamResponseBodyWithByteLimit: coerce<
|
|
2246
|
+
TtsRouteArg["streamResponseBodyWithByteLimit"]
|
|
2247
|
+
>(streamResponseBodyWithByteLimit),
|
|
2248
|
+
responseContentLength,
|
|
2249
|
+
isAbortError,
|
|
2250
|
+
ELEVENLABS_FETCH_TIMEOUT_MS: 30_000,
|
|
2251
|
+
ELEVENLABS_AUDIO_MAX_BYTES: 20 * 1_048_576,
|
|
2252
|
+
})
|
|
2253
|
+
) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// ── Avatar routes (extracted to avatar-routes.ts) ───────────────────
|
|
2258
|
+
if (
|
|
2259
|
+
await handleAvatarRoutes({
|
|
2260
|
+
req,
|
|
2261
|
+
res,
|
|
2262
|
+
method,
|
|
2263
|
+
pathname,
|
|
2264
|
+
json,
|
|
2265
|
+
error,
|
|
2266
|
+
})
|
|
2267
|
+
) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2272
|
+
// Config routes (extracted to config-routes.ts)
|
|
2273
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2274
|
+
if (
|
|
2275
|
+
pathname === "/api/config" ||
|
|
2276
|
+
pathname === "/api/config/schema" ||
|
|
2277
|
+
pathname === "/api/config/reload"
|
|
2278
|
+
) {
|
|
2279
|
+
if (
|
|
2280
|
+
await handleConfigRoutes({
|
|
2281
|
+
req,
|
|
2282
|
+
res,
|
|
2283
|
+
method,
|
|
2284
|
+
pathname,
|
|
2285
|
+
url,
|
|
2286
|
+
config: state.config,
|
|
2287
|
+
runtime: state.runtime,
|
|
2288
|
+
json,
|
|
2289
|
+
error,
|
|
2290
|
+
readJsonBody,
|
|
2291
|
+
redactConfigSecrets,
|
|
2292
|
+
isBlockedObjectKey,
|
|
2293
|
+
stripRedactedPlaceholderValuesDeep,
|
|
2294
|
+
patchTouchesProviderSelection,
|
|
2295
|
+
BLOCKED_ENV_KEYS,
|
|
2296
|
+
CONFIG_WRITE_ALLOWED_TOP_KEYS,
|
|
2297
|
+
resolveMcpServersRejection,
|
|
2298
|
+
resolveMcpTerminalAuthorizationRejection,
|
|
2299
|
+
})
|
|
2300
|
+
) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// ── Permissions extra routes (extracted to permissions-routes-extra.ts) ──
|
|
2306
|
+
if (
|
|
2307
|
+
await handlePermissionsExtraRoutes({
|
|
2308
|
+
req,
|
|
2309
|
+
res,
|
|
2310
|
+
method,
|
|
2311
|
+
pathname,
|
|
2312
|
+
state: coerce<PermissionsExtraRouteArg["state"]>(state),
|
|
2313
|
+
json,
|
|
2314
|
+
error,
|
|
2315
|
+
readJsonBody,
|
|
2316
|
+
saveElizaConfig,
|
|
2317
|
+
resolveTradePermissionMode: coerce<
|
|
2318
|
+
PermissionsExtraRouteArg["resolveTradePermissionMode"]
|
|
2319
|
+
>(resolveTradePermissionMode),
|
|
2320
|
+
canUseLocalTradeExecution: coerce<
|
|
2321
|
+
PermissionsExtraRouteArg["canUseLocalTradeExecution"]
|
|
2322
|
+
>(canUseLocalTradeExecution),
|
|
2323
|
+
parseAgentAutomationMode,
|
|
2324
|
+
persistAgentAutomationMode: coerce<
|
|
2325
|
+
PermissionsExtraRouteArg["persistAgentAutomationMode"]
|
|
2326
|
+
>(persistAgentAutomationMode),
|
|
2327
|
+
})
|
|
2328
|
+
) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (
|
|
2333
|
+
await handlePermissionRoutes({
|
|
2334
|
+
req,
|
|
2335
|
+
res,
|
|
2336
|
+
method,
|
|
2337
|
+
pathname,
|
|
2338
|
+
state,
|
|
2339
|
+
readJsonBody,
|
|
2340
|
+
json,
|
|
2341
|
+
error,
|
|
2342
|
+
saveConfig: (config) => {
|
|
2343
|
+
saveElizaConfig(config as ElizaConfig);
|
|
2344
|
+
},
|
|
2345
|
+
scheduleRuntimeRestart,
|
|
2346
|
+
})
|
|
2347
|
+
) {
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (
|
|
2352
|
+
await handleRelationshipsRoutes({
|
|
2353
|
+
req,
|
|
2354
|
+
res,
|
|
2355
|
+
method,
|
|
2356
|
+
pathname,
|
|
2357
|
+
runtime: state.runtime ?? undefined,
|
|
2358
|
+
readJsonBody,
|
|
2359
|
+
json,
|
|
2360
|
+
error,
|
|
2361
|
+
})
|
|
2362
|
+
) {
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
if (
|
|
2367
|
+
await handleBrowserWorkspaceRoutes({
|
|
2368
|
+
req,
|
|
2369
|
+
res,
|
|
2370
|
+
method,
|
|
2371
|
+
pathname,
|
|
2372
|
+
readJsonBody,
|
|
2373
|
+
json,
|
|
2374
|
+
error,
|
|
2375
|
+
})
|
|
2376
|
+
) {
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// Agent self-status, Privy, and ERC-8004 registry routes are now handled
|
|
2381
|
+
// by handleAgentStatusRoutes above.
|
|
2382
|
+
|
|
2383
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2384
|
+
// Subscription status route
|
|
2385
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2386
|
+
|
|
2387
|
+
// ── GET /api/subscription/status (direct handler fallback) ─────────────
|
|
2388
|
+
// Note: subscription-routes.ts handles /api/subscription/* but this is
|
|
2389
|
+
// kept here in case the prefix routing is not active.
|
|
2390
|
+
// (handleSubscriptionRoutes already covers this, so no duplicate needed.)
|
|
2391
|
+
|
|
2392
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2393
|
+
// BSC trade routes and wallet trade execute — now handled by
|
|
2394
|
+
// @elizaos/app-steward plugin routes. See apps/app-steward/src/plugin.ts.
|
|
2395
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2396
|
+
|
|
2397
|
+
if (
|
|
2398
|
+
isLifeOpsCloudPluginRoute(pathname) &&
|
|
2399
|
+
(await tryHandleRuntimePluginRoute({
|
|
2400
|
+
req,
|
|
2401
|
+
res,
|
|
2402
|
+
method,
|
|
2403
|
+
pathname,
|
|
2404
|
+
url,
|
|
2405
|
+
runtime: state.runtime,
|
|
2406
|
+
isAuthorized: () => isAuthorized(req),
|
|
2407
|
+
}))
|
|
2408
|
+
) {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// ── Cloud routes (/api/cloud/*) ─────────────────────────────────────────
|
|
2413
|
+
if (pathname.startsWith("/api/cloud/")) {
|
|
2414
|
+
const xRelayHandled = await handleXRelayRoute(req, res, pathname, method, {
|
|
2415
|
+
config: state.config,
|
|
2416
|
+
runtime: state.runtime,
|
|
2417
|
+
});
|
|
2418
|
+
if (xRelayHandled) return;
|
|
2419
|
+
|
|
2420
|
+
const billingHandled = await handleCloudBillingRoute(
|
|
2421
|
+
req,
|
|
2422
|
+
res,
|
|
2423
|
+
pathname,
|
|
2424
|
+
method,
|
|
2425
|
+
{ config: state.config, runtime: state.runtime },
|
|
2426
|
+
);
|
|
2427
|
+
if (billingHandled) return;
|
|
2428
|
+
|
|
2429
|
+
// Compat proxy routes — transparent proxy to Eliza Cloud v2 /api/compat/*
|
|
2430
|
+
const compatHandled = await handleCloudCompatRoute(
|
|
2431
|
+
req,
|
|
2432
|
+
res,
|
|
2433
|
+
pathname,
|
|
2434
|
+
method,
|
|
2435
|
+
{ config: state.config, runtime: state.runtime },
|
|
2436
|
+
);
|
|
2437
|
+
if (compatHandled) return;
|
|
2438
|
+
|
|
2439
|
+
const cloudState: CloudRouteState = {
|
|
2440
|
+
config: state.config,
|
|
2441
|
+
cloudManager: state.cloudManager,
|
|
2442
|
+
runtime: state.runtime,
|
|
2443
|
+
saveConfig: saveElizaConfig,
|
|
2444
|
+
createTelemetrySpan: createIntegrationTelemetrySpan,
|
|
2445
|
+
restartRuntime,
|
|
2446
|
+
};
|
|
2447
|
+
const handled = await handleCloudRoute(
|
|
2448
|
+
req,
|
|
2449
|
+
res,
|
|
2450
|
+
pathname,
|
|
2451
|
+
method,
|
|
2452
|
+
cloudState,
|
|
2453
|
+
);
|
|
2454
|
+
if (handled) return;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ── Sandbox routes (/api/sandbox/*) ────────────────────────────────────
|
|
2458
|
+
if (pathname.startsWith("/api/sandbox")) {
|
|
2459
|
+
const handled = await handleSandboxRoute(req, res, pathname, method, {
|
|
2460
|
+
sandboxManager: state.sandboxManager,
|
|
2461
|
+
});
|
|
2462
|
+
if (handled) return;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2466
|
+
// Conversation routes (/api/conversations/*) — delegated to conversation-routes.ts
|
|
2467
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2468
|
+
|
|
2469
|
+
if (pathname.startsWith("/api/conversations")) {
|
|
2470
|
+
// Cast state — ConversationRouteState is a compatible subset of ServerState
|
|
2471
|
+
const handled = await handleConversationRoutes({
|
|
2472
|
+
req,
|
|
2473
|
+
res,
|
|
2474
|
+
method,
|
|
2475
|
+
pathname,
|
|
2476
|
+
readJsonBody,
|
|
2477
|
+
json,
|
|
2478
|
+
error,
|
|
2479
|
+
state: coerce<ConversationRouteArg["state"]>(state),
|
|
2480
|
+
});
|
|
2481
|
+
if (handled) return;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2485
|
+
// OpenAI-compatible routes (/v1/*) — delegated to chat-routes.ts
|
|
2486
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2487
|
+
|
|
2488
|
+
if (pathname.startsWith("/v1/")) {
|
|
2489
|
+
// Cast state — ChatRouteState is a compatible subset of ServerState
|
|
2490
|
+
const handled = await handleChatRoutes({
|
|
2491
|
+
req,
|
|
2492
|
+
res,
|
|
2493
|
+
method,
|
|
2494
|
+
pathname,
|
|
2495
|
+
readJsonBody,
|
|
2496
|
+
json,
|
|
2497
|
+
error,
|
|
2498
|
+
state: coerce<ChatRouteArg["state"]>(state),
|
|
2499
|
+
});
|
|
2500
|
+
if (handled) return;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// ── Database management API ─────────────────────────────────────────────
|
|
2504
|
+
if (pathname.startsWith("/api/database/")) {
|
|
2505
|
+
const handled = await handleDatabaseRoute(
|
|
2506
|
+
req,
|
|
2507
|
+
res,
|
|
2508
|
+
state.runtime,
|
|
2509
|
+
pathname,
|
|
2510
|
+
);
|
|
2511
|
+
if (handled) return;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// ── Trajectory management API ──────────────────────────────────────────
|
|
2515
|
+
if (pathname.startsWith("/api/trajectories")) {
|
|
2516
|
+
if (!state.runtime) {
|
|
2517
|
+
sendJsonError(res, "Agent runtime not started yet", 503);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const handled = await handleTrajectoryRoute(
|
|
2521
|
+
req,
|
|
2522
|
+
res,
|
|
2523
|
+
state.runtime,
|
|
2524
|
+
pathname,
|
|
2525
|
+
method,
|
|
2526
|
+
);
|
|
2527
|
+
if (handled) return;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// ── Coding Agent API (/api/coding-agents/*, /api/workspace/*, /api/issues/*) ──
|
|
2531
|
+
if (
|
|
2532
|
+
!state.runtime &&
|
|
2533
|
+
method === "GET" &&
|
|
2534
|
+
pathname === "/api/coding-agents/coordinator/status"
|
|
2535
|
+
) {
|
|
2536
|
+
error(res, "Coding agent runtime unavailable", 503);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (
|
|
2540
|
+
!state.runtime &&
|
|
2541
|
+
method === "GET" &&
|
|
2542
|
+
pathname === "/api/coding-agents/preflight"
|
|
2543
|
+
) {
|
|
2544
|
+
error(res, "Coding agent runtime unavailable", 503);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (
|
|
2548
|
+
!state.runtime &&
|
|
2549
|
+
method === "GET" &&
|
|
2550
|
+
pathname.startsWith("/api/coding-agents")
|
|
2551
|
+
) {
|
|
2552
|
+
error(res, "Coding agent runtime unavailable", 503);
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
if (
|
|
2556
|
+
state.runtime &&
|
|
2557
|
+
(pathname.startsWith("/api/coding-agents") ||
|
|
2558
|
+
pathname.startsWith("/api/workspace") ||
|
|
2559
|
+
pathname.startsWith("/api/issues"))
|
|
2560
|
+
) {
|
|
2561
|
+
const isCoordinatorStatusRoute =
|
|
2562
|
+
method === "GET" && pathname === "/api/coding-agents/coordinator/status";
|
|
2563
|
+
const isPreflightRoute =
|
|
2564
|
+
method === "GET" && pathname === "/api/coding-agents/preflight";
|
|
2565
|
+
|
|
2566
|
+
// Try to dynamically load the route handler from the local plugin first
|
|
2567
|
+
let handled = false;
|
|
2568
|
+
|
|
2569
|
+
// Lazily start PTY_SERVICE if it was registered but not yet started.
|
|
2570
|
+
// The core runtime only starts services on-demand via getServiceLoadPromise,
|
|
2571
|
+
// but the orchestrator plugin's route handler checks getService() (which
|
|
2572
|
+
// only returns already-started instances). Without this kick, the plugin
|
|
2573
|
+
// sees null and returns 503 for every route.
|
|
2574
|
+
if (
|
|
2575
|
+
!state.runtime.getService("PTY_SERVICE") &&
|
|
2576
|
+
state.runtime.hasService("PTY_SERVICE")
|
|
2577
|
+
) {
|
|
2578
|
+
try {
|
|
2579
|
+
await state.runtime.getServiceLoadPromise("PTY_SERVICE");
|
|
2580
|
+
wireCodingAgentBridgesNow(state);
|
|
2581
|
+
} catch {
|
|
2582
|
+
// Service start failed — the fallback handler will surface 503 unavailability.
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
const ptyService = state.runtime.getService(
|
|
2587
|
+
"PTY_SERVICE",
|
|
2588
|
+
) as PTYService | null;
|
|
2589
|
+
const coordinator = getCoordinatorFromRuntime(state.runtime);
|
|
2590
|
+
const codeTaskService = state.runtime.getService("CODE_TASK");
|
|
2591
|
+
const isTaskRoute =
|
|
2592
|
+
method === "GET" && pathname === "/api/coding-agents/tasks";
|
|
2593
|
+
const isTaskDetailRoute =
|
|
2594
|
+
method === "GET" && /^\/api\/coding-agents\/tasks\/[^/]+$/.test(pathname);
|
|
2595
|
+
const isSessionsRoute =
|
|
2596
|
+
method === "GET" && pathname === "/api/coding-agents/sessions";
|
|
2597
|
+
const isSessionDetailRoute =
|
|
2598
|
+
method === "GET" &&
|
|
2599
|
+
/^\/api\/coding-agents\/sessions\/[^/]+$/.test(pathname);
|
|
2600
|
+
const isScratchRoute =
|
|
2601
|
+
method === "GET" && pathname === "/api/coding-agents/scratch";
|
|
2602
|
+
const isAgentListRoute =
|
|
2603
|
+
method === "GET" && pathname === "/api/coding-agents";
|
|
2604
|
+
|
|
2605
|
+
// The settings UI and startup hydration poll these routes early. When the
|
|
2606
|
+
// PTY/coordinator services are not ready yet, surface explicit 503
|
|
2607
|
+
// unavailability rather than synthesizing success-shaped empty payloads.
|
|
2608
|
+
if (
|
|
2609
|
+
(isCoordinatorStatusRoute && !coordinator) ||
|
|
2610
|
+
(isPreflightRoute && !ptyService) ||
|
|
2611
|
+
((isTaskRoute ||
|
|
2612
|
+
isTaskDetailRoute ||
|
|
2613
|
+
isScratchRoute ||
|
|
2614
|
+
isAgentListRoute) &&
|
|
2615
|
+
!codeTaskService) ||
|
|
2616
|
+
((isSessionsRoute || isSessionDetailRoute) && !ptyService)
|
|
2617
|
+
) {
|
|
2618
|
+
handled = await handleCodingAgentsFallback(
|
|
2619
|
+
state.runtime,
|
|
2620
|
+
pathname,
|
|
2621
|
+
method,
|
|
2622
|
+
req,
|
|
2623
|
+
res,
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// Prefer @elizaos/plugin-agent-orchestrator route handler so the full coordinator
|
|
2628
|
+
// contract is served from the embedded runtime (replaces the old plugin).
|
|
2629
|
+
if (!handled)
|
|
2630
|
+
try {
|
|
2631
|
+
const orchestratorPlugin =
|
|
2632
|
+
agentOrchestratorCompat as OrchestratorPluginFallbackModule | null;
|
|
2633
|
+
if (orchestratorPlugin?.createCodingAgentRouteHandler) {
|
|
2634
|
+
const coordinator = orchestratorPlugin.getCoordinator?.(
|
|
2635
|
+
state.runtime,
|
|
2636
|
+
);
|
|
2637
|
+
const handler = orchestratorPlugin.createCodingAgentRouteHandler(
|
|
2638
|
+
state.runtime,
|
|
2639
|
+
coordinator,
|
|
2640
|
+
);
|
|
2641
|
+
handled = await (handler as ConnectorRouteHandler)(
|
|
2642
|
+
req,
|
|
2643
|
+
res,
|
|
2644
|
+
pathname,
|
|
2645
|
+
req.method ?? "GET",
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
} catch {
|
|
2649
|
+
// Compat layer unavailable — final fallback below handles coding-agents routes.
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// Final fallback: handle coding-agents routes using the plugin's CODE_TASK compatibility service.
|
|
2653
|
+
if (!handled && pathname.startsWith("/api/coding-agents")) {
|
|
2654
|
+
handled = await handleCodingAgentsFallback(
|
|
2655
|
+
state.runtime,
|
|
2656
|
+
pathname,
|
|
2657
|
+
method,
|
|
2658
|
+
req,
|
|
2659
|
+
res,
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
if (handled) return;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
if (
|
|
2667
|
+
await handleCloudStatusRoutes({
|
|
2668
|
+
req,
|
|
2669
|
+
res,
|
|
2670
|
+
method,
|
|
2671
|
+
pathname,
|
|
2672
|
+
config: state.config,
|
|
2673
|
+
runtime: state.runtime,
|
|
2674
|
+
json,
|
|
2675
|
+
})
|
|
2676
|
+
) {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// ── App routes (/api/apps/*) ──────────────────────────────────────────
|
|
2681
|
+
if (
|
|
2682
|
+
await handleAppsRoutes({
|
|
2683
|
+
req,
|
|
2684
|
+
res,
|
|
2685
|
+
method,
|
|
2686
|
+
pathname,
|
|
2687
|
+
url,
|
|
2688
|
+
appManager: {
|
|
2689
|
+
listAvailable: (pluginManager) =>
|
|
2690
|
+
state.appManager.listAvailable(pluginManager),
|
|
2691
|
+
search: (pluginManager, query, limit) =>
|
|
2692
|
+
state.appManager.search(pluginManager, query, limit),
|
|
2693
|
+
listInstalled: (pluginManager) =>
|
|
2694
|
+
state.appManager.listInstalled(pluginManager),
|
|
2695
|
+
listRuns: (runtime) =>
|
|
2696
|
+
state.appManager.listRuns(
|
|
2697
|
+
runtime && typeof runtime === "object"
|
|
2698
|
+
? (runtime as IAgentRuntime)
|
|
2699
|
+
: null,
|
|
2700
|
+
),
|
|
2701
|
+
getRun: (runId, runtime) =>
|
|
2702
|
+
state.appManager.getRun(
|
|
2703
|
+
runId,
|
|
2704
|
+
runtime && typeof runtime === "object"
|
|
2705
|
+
? (runtime as IAgentRuntime)
|
|
2706
|
+
: null,
|
|
2707
|
+
),
|
|
2708
|
+
attachRun: (runId, runtime) =>
|
|
2709
|
+
state.appManager.attachRun(
|
|
2710
|
+
runId,
|
|
2711
|
+
runtime && typeof runtime === "object"
|
|
2712
|
+
? (runtime as IAgentRuntime)
|
|
2713
|
+
: null,
|
|
2714
|
+
),
|
|
2715
|
+
detachRun: (runId) => state.appManager.detachRun(runId),
|
|
2716
|
+
launch: (pluginManager, name, onProgress, runtime) =>
|
|
2717
|
+
state.appManager.launch(
|
|
2718
|
+
pluginManager,
|
|
2719
|
+
name,
|
|
2720
|
+
onProgress,
|
|
2721
|
+
runtime && typeof runtime === "object"
|
|
2722
|
+
? (runtime as IAgentRuntime)
|
|
2723
|
+
: null,
|
|
2724
|
+
),
|
|
2725
|
+
stop: (pluginManager, name, runId, runtime) =>
|
|
2726
|
+
state.appManager.stop(
|
|
2727
|
+
pluginManager,
|
|
2728
|
+
name,
|
|
2729
|
+
runId,
|
|
2730
|
+
runtime && typeof runtime === "object"
|
|
2731
|
+
? (runtime as IAgentRuntime)
|
|
2732
|
+
: null,
|
|
2733
|
+
),
|
|
2734
|
+
recordHeartbeat: (runId) => state.appManager.recordHeartbeat(runId),
|
|
2735
|
+
getInfo: (pluginManager, name) =>
|
|
2736
|
+
state.appManager.getInfo(pluginManager, name),
|
|
2737
|
+
},
|
|
2738
|
+
getPluginManager: () => getPluginManagerForState(state),
|
|
2739
|
+
parseBoundedLimit,
|
|
2740
|
+
readJsonBody,
|
|
2741
|
+
json,
|
|
2742
|
+
error,
|
|
2743
|
+
runtime: state.runtime,
|
|
2744
|
+
favoriteApps: {
|
|
2745
|
+
read: () => readFavoriteAppsFromConfig(state.config),
|
|
2746
|
+
write: (apps) => writeFavoriteAppsToConfig(state.config, apps),
|
|
2747
|
+
},
|
|
2748
|
+
})
|
|
2749
|
+
) {
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
if (
|
|
2754
|
+
await handleAppPackageRoutes({
|
|
2755
|
+
req,
|
|
2756
|
+
res,
|
|
2757
|
+
method,
|
|
2758
|
+
pathname,
|
|
2759
|
+
url,
|
|
2760
|
+
readJsonBody,
|
|
2761
|
+
json,
|
|
2762
|
+
error,
|
|
2763
|
+
runtime: state.runtime,
|
|
2764
|
+
})
|
|
2765
|
+
) {
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2770
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2771
|
+
// Workbench routes (extracted to workbench-routes.ts)
|
|
2772
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2773
|
+
if (pathname.startsWith("/api/workbench")) {
|
|
2774
|
+
if (
|
|
2775
|
+
await handleWorkbenchRoutes({
|
|
2776
|
+
req,
|
|
2777
|
+
res,
|
|
2778
|
+
method,
|
|
2779
|
+
pathname,
|
|
2780
|
+
url,
|
|
2781
|
+
state: coerce<WorkbenchRouteArg["state"]>(state),
|
|
2782
|
+
json,
|
|
2783
|
+
error,
|
|
2784
|
+
readJsonBody,
|
|
2785
|
+
toWorkbenchTask:
|
|
2786
|
+
coerce<WorkbenchRouteArg["toWorkbenchTask"]>(toWorkbenchTask),
|
|
2787
|
+
toWorkbenchTodo:
|
|
2788
|
+
coerce<WorkbenchRouteArg["toWorkbenchTodo"]>(toWorkbenchTodo),
|
|
2789
|
+
normalizeTags,
|
|
2790
|
+
readTaskMetadata,
|
|
2791
|
+
readTaskCompleted,
|
|
2792
|
+
parseNullableNumber,
|
|
2793
|
+
asObject,
|
|
2794
|
+
decodePathComponent,
|
|
2795
|
+
taskToTriggerSummary:
|
|
2796
|
+
coerce<WorkbenchRouteArg["taskToTriggerSummary"]>(
|
|
2797
|
+
taskToTriggerSummary,
|
|
2798
|
+
),
|
|
2799
|
+
listTriggerTasks:
|
|
2800
|
+
coerce<WorkbenchRouteArg["listTriggerTasks"]>(listTriggerTasks),
|
|
2801
|
+
})
|
|
2802
|
+
) {
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2808
|
+
// Life-ops routes: now served via lifeopsPlugin.routes (rawPath) on the
|
|
2809
|
+
// runtime plugin route system. See app-lifeops/src/routes/plugin.ts.
|
|
2810
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2811
|
+
|
|
2812
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2813
|
+
// MCP routes (extracted to mcp-routes.ts)
|
|
2814
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2815
|
+
if (pathname.startsWith("/api/mcp")) {
|
|
2816
|
+
if (
|
|
2817
|
+
await handleMcpRoutes({
|
|
2818
|
+
req,
|
|
2819
|
+
res,
|
|
2820
|
+
method,
|
|
2821
|
+
pathname,
|
|
2822
|
+
url,
|
|
2823
|
+
state,
|
|
2824
|
+
json,
|
|
2825
|
+
error,
|
|
2826
|
+
readJsonBody,
|
|
2827
|
+
saveElizaConfig,
|
|
2828
|
+
redactDeep,
|
|
2829
|
+
isBlockedObjectKey,
|
|
2830
|
+
cloneWithoutBlockedObjectKeys,
|
|
2831
|
+
resolveMcpServersRejection,
|
|
2832
|
+
resolveMcpTerminalAuthorizationRejection,
|
|
2833
|
+
decodePathComponent,
|
|
2834
|
+
})
|
|
2835
|
+
) {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2841
|
+
// Misc routes (extracted to misc-routes.ts)
|
|
2842
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2843
|
+
if (
|
|
2844
|
+
await handleMiscRoutes({
|
|
2845
|
+
req,
|
|
2846
|
+
res,
|
|
2847
|
+
method,
|
|
2848
|
+
pathname,
|
|
2849
|
+
url,
|
|
2850
|
+
state: coerce<MiscRouteArg["state"]>(state),
|
|
2851
|
+
json,
|
|
2852
|
+
error,
|
|
2853
|
+
readJsonBody,
|
|
2854
|
+
AGENT_EVENT_ALLOWED_STREAMS,
|
|
2855
|
+
resolveTerminalRunRejection,
|
|
2856
|
+
resolveTerminalRunClientId,
|
|
2857
|
+
isSharedTerminalClientId,
|
|
2858
|
+
activeTerminalRunCount,
|
|
2859
|
+
setActiveTerminalRunCount: (delta: number) => {
|
|
2860
|
+
activeTerminalRunCount = Math.max(0, activeTerminalRunCount + delta);
|
|
2861
|
+
},
|
|
2862
|
+
})
|
|
2863
|
+
) {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
if (
|
|
2868
|
+
await handleWhatsAppRoute(
|
|
2869
|
+
req,
|
|
2870
|
+
res,
|
|
2871
|
+
pathname,
|
|
2872
|
+
method,
|
|
2873
|
+
{
|
|
2874
|
+
whatsappPairingSessions: state.whatsappPairingSessions ?? new Map(),
|
|
2875
|
+
broadcastWs: state.broadcastWs ?? undefined,
|
|
2876
|
+
config: state.config,
|
|
2877
|
+
runtime: state.runtime ?? undefined,
|
|
2878
|
+
saveConfig: () => saveElizaConfig(state.config),
|
|
2879
|
+
workspaceDir: resolveDefaultAgentWorkspaceDir(),
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
sanitizeAccountId: sanitizeWhatsAppAccountId,
|
|
2883
|
+
whatsappAuthExists,
|
|
2884
|
+
whatsappLogout,
|
|
2885
|
+
createWhatsAppPairingSession: (options) =>
|
|
2886
|
+
new WhatsAppPairingSession(options),
|
|
2887
|
+
},
|
|
2888
|
+
)
|
|
2889
|
+
) {
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// ── elizaOS plugin HTTP routes (runtime.routes, e.g. /music-player/*) ───
|
|
2894
|
+
if (
|
|
2895
|
+
await tryHandleRuntimePluginRoute({
|
|
2896
|
+
req,
|
|
2897
|
+
res,
|
|
2898
|
+
method,
|
|
2899
|
+
pathname,
|
|
2900
|
+
url,
|
|
2901
|
+
runtime: state.runtime,
|
|
2902
|
+
isAuthorized: () => isAuthorized(req),
|
|
2903
|
+
})
|
|
2904
|
+
) {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// ── Connector plugin routes (dynamically registered) ────────────────────
|
|
2909
|
+
for (const handler of state.connectorRouteHandlers) {
|
|
2910
|
+
const handled = await handler(req, res, pathname, method);
|
|
2911
|
+
if (handled) return;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// ── Music player compatibility fallback ─────────────────────────────────
|
|
2915
|
+
if (
|
|
2916
|
+
tryHandleMusicPlayerStatusFallback({
|
|
2917
|
+
pathname,
|
|
2918
|
+
method,
|
|
2919
|
+
runtime: state.runtime,
|
|
2920
|
+
res,
|
|
2921
|
+
})
|
|
2922
|
+
) {
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// ── Fallback ────────────────────────────────────────────────────────────
|
|
2927
|
+
error(res, "Not found", 404);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// ---------------------------------------------------------------------------
|
|
2931
|
+
// Early log capture — re-exported from the standalone module so existing
|
|
2932
|
+
// callers that `import { captureEarlyLogs } from "../../../../src/api/server"` keep
|
|
2933
|
+
// working. The implementation lives in `./early-logs.ts` to avoid pulling
|
|
2934
|
+
// the entire server dependency graph into lightweight consumers (e.g. the
|
|
2935
|
+
// headless `startEliza()` path).
|
|
2936
|
+
// ---------------------------------------------------------------------------
|
|
2937
|
+
import { type captureEarlyLogs, flushEarlyLogs } from "./early-logs.js";
|
|
2938
|
+
|
|
2939
|
+
export type { captureEarlyLogs };
|
|
2940
|
+
|
|
2941
|
+
// ---------------------------------------------------------------------------
|
|
2942
|
+
// Server start
|
|
2943
|
+
// ---------------------------------------------------------------------------
|
|
2944
|
+
|
|
2945
|
+
export async function startApiServer(opts?: {
|
|
2946
|
+
port?: number;
|
|
2947
|
+
runtime?: AgentRuntime;
|
|
2948
|
+
skipDeferredStartupWork?: boolean;
|
|
2949
|
+
/** Initial state when starting without a runtime (e.g. embedded startup flow). */
|
|
2950
|
+
initialAgentState?: "not_started" | "starting" | "stopped" | "error";
|
|
2951
|
+
/**
|
|
2952
|
+
* Called when the UI requests a restart via `POST /api/agent/restart`.
|
|
2953
|
+
* Should stop the current runtime, create a new one, and return it.
|
|
2954
|
+
* If omitted the endpoint returns 501 (not supported in this mode).
|
|
2955
|
+
*/
|
|
2956
|
+
onRestart?: () => Promise<AgentRuntime | null>;
|
|
2957
|
+
}): Promise<{
|
|
2958
|
+
port: number;
|
|
2959
|
+
close: () => Promise<void>;
|
|
2960
|
+
updateRuntime: (rt: AgentRuntime) => void;
|
|
2961
|
+
updateStartup: (
|
|
2962
|
+
update: Partial<AgentStartupDiagnostics> & {
|
|
2963
|
+
phase?: string;
|
|
2964
|
+
attempt?: number;
|
|
2965
|
+
state?: ServerState["agentState"];
|
|
2966
|
+
},
|
|
2967
|
+
) => void;
|
|
2968
|
+
}> {
|
|
2969
|
+
const apiStartTime = Date.now();
|
|
2970
|
+
console.log(`[eliza-api] startApiServer called`);
|
|
2971
|
+
|
|
2972
|
+
const port = opts?.port ?? resolveServerOnlyPort(process.env);
|
|
2973
|
+
const host = resolveApiBindHost(process.env);
|
|
2974
|
+
ensureApiTokenForBindHost(host);
|
|
2975
|
+
console.log(`[eliza-api] Token check done (${Date.now() - apiStartTime}ms)`);
|
|
2976
|
+
|
|
2977
|
+
let config: ElizaConfig;
|
|
2978
|
+
try {
|
|
2979
|
+
config = loadElizaConfig();
|
|
2980
|
+
} catch (err) {
|
|
2981
|
+
logger.warn(
|
|
2982
|
+
`[eliza-api] Failed to load config, starting with defaults: ${err instanceof Error ? err.message : err}`,
|
|
2983
|
+
);
|
|
2984
|
+
config = {} as ElizaConfig;
|
|
2985
|
+
}
|
|
2986
|
+
console.log(`[eliza-api] Config loaded (${Date.now() - apiStartTime}ms)`);
|
|
2987
|
+
|
|
2988
|
+
// Wallet/inventory routes read from process.env at request-time.
|
|
2989
|
+
// Hydrate persisted config.env values so addresses remain visible after restarts.
|
|
2990
|
+
const persistedEnv = config.env as Record<string, string> | undefined;
|
|
2991
|
+
const envKeysToHydrate = [
|
|
2992
|
+
"ELIZA_WALLET_OS_STORE",
|
|
2993
|
+
"EVM_PRIVATE_KEY",
|
|
2994
|
+
"SOLANA_PRIVATE_KEY",
|
|
2995
|
+
"ALCHEMY_API_KEY",
|
|
2996
|
+
"INFURA_API_KEY",
|
|
2997
|
+
"ANKR_API_KEY",
|
|
2998
|
+
"HELIUS_API_KEY",
|
|
2999
|
+
"BIRDEYE_API_KEY",
|
|
3000
|
+
"SOLANA_RPC_URL",
|
|
3001
|
+
] as const;
|
|
3002
|
+
for (const key of envKeysToHydrate) {
|
|
3003
|
+
const value = persistedEnv?.[key];
|
|
3004
|
+
if (typeof value === "string" && value.trim() && !process.env[key]) {
|
|
3005
|
+
process.env[key] = value.trim();
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// Optional auto-provision mode for legacy environments. Disabled by default
|
|
3010
|
+
// so startup does not silently create new wallets when keys are missing.
|
|
3011
|
+
const walletAutoProvisionRaw =
|
|
3012
|
+
process.env.ELIZA_WALLET_AUTO_PROVISION?.trim().toLowerCase();
|
|
3013
|
+
const walletAutoProvisionEnabled =
|
|
3014
|
+
walletAutoProvisionRaw === "1" ||
|
|
3015
|
+
walletAutoProvisionRaw === "true" ||
|
|
3016
|
+
walletAutoProvisionRaw === "on" ||
|
|
3017
|
+
walletAutoProvisionRaw === "yes";
|
|
3018
|
+
if (walletAutoProvisionEnabled && ensureWalletKeysInEnvAndConfig(config)) {
|
|
3019
|
+
try {
|
|
3020
|
+
saveElizaConfig(config);
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
logger.warn(
|
|
3023
|
+
`[eliza-api] Failed to persist generated wallet keys: ${err instanceof Error ? err.message : err}`,
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// Pre-load steward wallet addresses so getWalletAddresses() has them
|
|
3029
|
+
// available synchronously from the start (cloud-provisioned containers).
|
|
3030
|
+
await initStewardWalletCache();
|
|
3031
|
+
|
|
3032
|
+
// Warn when wallet private keys live in plaintext config and the OS secure
|
|
3033
|
+
// store is not enabled. This nudges operators toward ELIZA_WALLET_OS_STORE=1.
|
|
3034
|
+
{
|
|
3035
|
+
const hasPlaintextKeys =
|
|
3036
|
+
(typeof persistedEnv?.EVM_PRIVATE_KEY === "string" &&
|
|
3037
|
+
persistedEnv.EVM_PRIVATE_KEY.trim()) ||
|
|
3038
|
+
(typeof persistedEnv?.SOLANA_PRIVATE_KEY === "string" &&
|
|
3039
|
+
persistedEnv.SOLANA_PRIVATE_KEY.trim());
|
|
3040
|
+
const osStoreRaw = process.env.ELIZA_WALLET_OS_STORE?.trim().toLowerCase();
|
|
3041
|
+
const osStoreEnabled =
|
|
3042
|
+
osStoreRaw === "1" ||
|
|
3043
|
+
osStoreRaw === "true" ||
|
|
3044
|
+
osStoreRaw === "on" ||
|
|
3045
|
+
osStoreRaw === "yes";
|
|
3046
|
+
if (hasPlaintextKeys && !osStoreEnabled) {
|
|
3047
|
+
logger.warn(
|
|
3048
|
+
"[wallet] Private keys are stored in plaintext config. " +
|
|
3049
|
+
"Set ELIZA_WALLET_OS_STORE=1 to use the OS secure store instead.",
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const plugins = discoverPluginsFromManifest();
|
|
3055
|
+
console.log(
|
|
3056
|
+
`[eliza-api] Plugins discovered (${Date.now() - apiStartTime}ms)`,
|
|
3057
|
+
);
|
|
3058
|
+
const workspaceDir =
|
|
3059
|
+
config.agents?.defaults?.workspace ?? resolveDefaultAgentWorkspaceDir();
|
|
3060
|
+
|
|
3061
|
+
const hasRuntime = opts?.runtime != null;
|
|
3062
|
+
const initialAgentState = hasRuntime
|
|
3063
|
+
? "running"
|
|
3064
|
+
: (opts?.initialAgentState ?? "not_started");
|
|
3065
|
+
const initialStartup: AgentStartupDiagnostics =
|
|
3066
|
+
initialAgentState === "running"
|
|
3067
|
+
? { phase: "running", attempt: 0 }
|
|
3068
|
+
: initialAgentState === "starting"
|
|
3069
|
+
? { phase: "starting", attempt: 0 }
|
|
3070
|
+
: { phase: "idle", attempt: 0 };
|
|
3071
|
+
const agentName = hasRuntime
|
|
3072
|
+
? (opts.runtime?.character.name ?? resolveDefaultAgentName(config))
|
|
3073
|
+
: resolveDefaultAgentName(config);
|
|
3074
|
+
|
|
3075
|
+
const deletedConversationIds = readDeletedConversationIdsFromState();
|
|
3076
|
+
|
|
3077
|
+
const state: ServerState = {
|
|
3078
|
+
runtime: opts?.runtime ?? null,
|
|
3079
|
+
config,
|
|
3080
|
+
agentState: initialAgentState,
|
|
3081
|
+
agentName,
|
|
3082
|
+
model: hasRuntime
|
|
3083
|
+
? detectRuntimeModel(opts.runtime ?? null, config)
|
|
3084
|
+
: undefined,
|
|
3085
|
+
startedAt:
|
|
3086
|
+
hasRuntime || initialAgentState === "starting" ? Date.now() : undefined,
|
|
3087
|
+
startup: initialStartup,
|
|
3088
|
+
plugins,
|
|
3089
|
+
// Filled asynchronously after server start to keep startup latency low.
|
|
3090
|
+
skills: [],
|
|
3091
|
+
logBuffer: [],
|
|
3092
|
+
eventBuffer: [],
|
|
3093
|
+
nextEventId: 1,
|
|
3094
|
+
chatRoomId: null,
|
|
3095
|
+
chatUserId: null,
|
|
3096
|
+
chatConnectionReady: null,
|
|
3097
|
+
chatConnectionPromise: null,
|
|
3098
|
+
adminEntityId: null,
|
|
3099
|
+
conversations: new Map(),
|
|
3100
|
+
conversationRestorePromise: null,
|
|
3101
|
+
deletedConversationIds,
|
|
3102
|
+
cloudManager: null,
|
|
3103
|
+
sandboxManager: null,
|
|
3104
|
+
appManager: new AppManager(),
|
|
3105
|
+
trainingService: null,
|
|
3106
|
+
registryService: null,
|
|
3107
|
+
dropService: null,
|
|
3108
|
+
shareIngestQueue: [],
|
|
3109
|
+
broadcastStatus: null,
|
|
3110
|
+
broadcastWs: null,
|
|
3111
|
+
broadcastWsToClientId: null,
|
|
3112
|
+
activeConversationId: null,
|
|
3113
|
+
permissionStates: {},
|
|
3114
|
+
shellEnabled: config.features?.shellEnabled !== false,
|
|
3115
|
+
agentAutomationMode: resolveAgentAutomationModeFromConfig(config),
|
|
3116
|
+
tradePermissionMode: resolveTradePermissionMode(config),
|
|
3117
|
+
pendingRestartReasons: [],
|
|
3118
|
+
connectorRouteHandlers: [],
|
|
3119
|
+
connectorHealthMonitor: null,
|
|
3120
|
+
whatsappPairingSessions: new Map(),
|
|
3121
|
+
};
|
|
3122
|
+
const trainingServiceCtor = await resolveTrainingServiceCtor();
|
|
3123
|
+
const trainingServiceOptions = {
|
|
3124
|
+
getRuntime: () => state.runtime,
|
|
3125
|
+
getConfig: () => state.config,
|
|
3126
|
+
setConfig: (nextConfig: ElizaConfig) => {
|
|
3127
|
+
state.config = nextConfig;
|
|
3128
|
+
saveElizaConfig(nextConfig);
|
|
3129
|
+
},
|
|
3130
|
+
};
|
|
3131
|
+
if (trainingServiceCtor) {
|
|
3132
|
+
state.trainingService = new trainingServiceCtor(trainingServiceOptions);
|
|
3133
|
+
} else {
|
|
3134
|
+
logger.info(
|
|
3135
|
+
"[eliza-api] Training service package unavailable; training routes will be disabled",
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
// Register immediately so /api/training routes are available without a startup race.
|
|
3139
|
+
const configuredAdminEntityId = config.agents?.defaults?.adminEntityId;
|
|
3140
|
+
if (configuredAdminEntityId && isUuidLike(configuredAdminEntityId)) {
|
|
3141
|
+
state.adminEntityId = configuredAdminEntityId;
|
|
3142
|
+
state.chatUserId = state.adminEntityId;
|
|
3143
|
+
} else if (configuredAdminEntityId) {
|
|
3144
|
+
logger.warn(
|
|
3145
|
+
`[eliza-api] Ignoring invalid agents.defaults.adminEntityId "${configuredAdminEntityId}"`,
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// Wire the app manager to the runtime if already running
|
|
3150
|
+
if (state.runtime) {
|
|
3151
|
+
// AppManager doesn't need a runtime reference — it just installs plugins
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// Start the periodic stale-run sweeper that stops app runs whose UI
|
|
3155
|
+
// heartbeat has gone silent (e.g. the user closed the tab without
|
|
3156
|
+
// pressing Stop). Without this, plugins that own a setInterval — like
|
|
3157
|
+
// the Defense-of-the-Agents game loop — would tick forever after the
|
|
3158
|
+
// browser disappeared. The sweeper invokes the same `stopRun` route
|
|
3159
|
+
// hook the Stop button uses so plugins have one shutdown path.
|
|
3160
|
+
state.appManager.startStaleRunSweeper(() => state.runtime);
|
|
3161
|
+
|
|
3162
|
+
const addLog = (
|
|
3163
|
+
level: string,
|
|
3164
|
+
message: string,
|
|
3165
|
+
source = "system",
|
|
3166
|
+
tags: string[] = [],
|
|
3167
|
+
) => {
|
|
3168
|
+
let resolvedSource = source;
|
|
3169
|
+
if (source === "auto" || source === "system") {
|
|
3170
|
+
const bracketMatch = /^\[([^\]]+)\]\s*/.exec(message);
|
|
3171
|
+
if (bracketMatch) resolvedSource = bracketMatch[1];
|
|
3172
|
+
}
|
|
3173
|
+
// Auto-tag based on source when no explicit tags provided
|
|
3174
|
+
const resolvedTags =
|
|
3175
|
+
tags.length > 0
|
|
3176
|
+
? tags
|
|
3177
|
+
: resolvedSource === "runtime" || resolvedSource === "autonomy"
|
|
3178
|
+
? ["agent"]
|
|
3179
|
+
: resolvedSource === "api" || resolvedSource === "websocket"
|
|
3180
|
+
? ["server"]
|
|
3181
|
+
: resolvedSource === "cloud"
|
|
3182
|
+
? ["server", "cloud"]
|
|
3183
|
+
: ["system"];
|
|
3184
|
+
pushWithBatchEvict(
|
|
3185
|
+
state.logBuffer,
|
|
3186
|
+
{
|
|
3187
|
+
timestamp: Date.now(),
|
|
3188
|
+
level,
|
|
3189
|
+
message,
|
|
3190
|
+
source: resolvedSource,
|
|
3191
|
+
tags: resolvedTags,
|
|
3192
|
+
},
|
|
3193
|
+
1200,
|
|
3194
|
+
200,
|
|
3195
|
+
);
|
|
3196
|
+
};
|
|
3197
|
+
|
|
3198
|
+
// ── Flush early-captured logs into the main buffer ────────────────────
|
|
3199
|
+
const earlyEntries = flushEarlyLogs();
|
|
3200
|
+
if (earlyEntries.length > 0) {
|
|
3201
|
+
for (const entry of earlyEntries) {
|
|
3202
|
+
state.logBuffer.push(entry);
|
|
3203
|
+
}
|
|
3204
|
+
if (state.logBuffer.length > 1000) {
|
|
3205
|
+
state.logBuffer.splice(0, state.logBuffer.length - 1000);
|
|
3206
|
+
}
|
|
3207
|
+
addLog(
|
|
3208
|
+
"info",
|
|
3209
|
+
`Flushed ${earlyEntries.length} early startup log entries`,
|
|
3210
|
+
"system",
|
|
3211
|
+
["system"],
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
addLog(
|
|
3216
|
+
"info",
|
|
3217
|
+
`Discovered ${plugins.length} plugins, loading skills in background`,
|
|
3218
|
+
"system",
|
|
3219
|
+
["system", "plugins"],
|
|
3220
|
+
);
|
|
3221
|
+
|
|
3222
|
+
// Warm per-provider model caches in background (non-blocking)
|
|
3223
|
+
void getOrFetchAllProviders().catch((err) => {
|
|
3224
|
+
logger.warn("[api] Provider cache warm-up failed:", err);
|
|
3225
|
+
});
|
|
3226
|
+
|
|
3227
|
+
// ── Intercept loggers so ALL agent/plugin/service logs appear in the UI ──
|
|
3228
|
+
// We patch both the global `logger` singleton from @elizaos/core (used by
|
|
3229
|
+
// eliza.ts, services, plugins, etc.) AND the runtime instance logger.
|
|
3230
|
+
// A marker prevents double-patching on hot-restart and avoids stacking
|
|
3231
|
+
// wrapper functions that would leak memory.
|
|
3232
|
+
const PATCHED_MARKER = "__elizaLogPatched";
|
|
3233
|
+
const LEVELS = ["debug", "info", "warn", "error"] as const;
|
|
3234
|
+
|
|
3235
|
+
/**
|
|
3236
|
+
* Patch a logger object so every log call also feeds into the UI log buffer.
|
|
3237
|
+
* Returns true if patching was performed, false if already patched.
|
|
3238
|
+
*/
|
|
3239
|
+
const patchLogger = (
|
|
3240
|
+
target: typeof logger,
|
|
3241
|
+
defaultSource: string,
|
|
3242
|
+
defaultTags: string[],
|
|
3243
|
+
): boolean => {
|
|
3244
|
+
const patchedTarget = target as typeof logger & {
|
|
3245
|
+
[PATCHED_MARKER]?: boolean;
|
|
3246
|
+
};
|
|
3247
|
+
if (patchedTarget[PATCHED_MARKER]) {
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
for (const lvl of LEVELS) {
|
|
3252
|
+
const original = target[lvl].bind(target);
|
|
3253
|
+
// pino / adze signature: logger.info(obj, msg) or logger.info(msg)
|
|
3254
|
+
const patched: (typeof target)[typeof lvl] = (
|
|
3255
|
+
...args: Parameters<typeof original>
|
|
3256
|
+
) => {
|
|
3257
|
+
let msg = "";
|
|
3258
|
+
let source = defaultSource;
|
|
3259
|
+
let tags = [...defaultTags];
|
|
3260
|
+
if (typeof args[0] === "string") {
|
|
3261
|
+
msg = args[0];
|
|
3262
|
+
} else if (args[0] && typeof args[0] === "object") {
|
|
3263
|
+
const obj = args[0] as Record<string, unknown>;
|
|
3264
|
+
if (typeof obj.src === "string") source = obj.src;
|
|
3265
|
+
// Extract tags from structured log objects
|
|
3266
|
+
if (Array.isArray(obj.tags)) {
|
|
3267
|
+
tags = [...tags, ...(obj.tags as string[])];
|
|
3268
|
+
}
|
|
3269
|
+
msg = typeof args[1] === "string" ? args[1] : JSON.stringify(obj);
|
|
3270
|
+
}
|
|
3271
|
+
// Auto-extract source from [bracket] prefixes (e.g. "[eliza] ...")
|
|
3272
|
+
const bracketMatch = /^\[([^\]]+)\]\s*/.exec(msg);
|
|
3273
|
+
if (bracketMatch && source === defaultSource) {
|
|
3274
|
+
source = bracketMatch[1];
|
|
3275
|
+
}
|
|
3276
|
+
// Auto-tag based on source context
|
|
3277
|
+
if (source !== defaultSource && !tags.includes(source)) {
|
|
3278
|
+
tags.push(source);
|
|
3279
|
+
}
|
|
3280
|
+
if (msg) addLog(lvl, msg, source, tags);
|
|
3281
|
+
return original(...args);
|
|
3282
|
+
};
|
|
3283
|
+
target[lvl] = patched;
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
patchedTarget[PATCHED_MARKER] = true;
|
|
3287
|
+
return true;
|
|
3288
|
+
};
|
|
3289
|
+
|
|
3290
|
+
// 1) Patch the global @elizaos/core logger — this captures ALL log calls
|
|
3291
|
+
// from eliza.ts, services, plugins, cloud, hooks, etc.
|
|
3292
|
+
if (patchLogger(logger, "agent", ["agent"])) {
|
|
3293
|
+
addLog(
|
|
3294
|
+
"info",
|
|
3295
|
+
"Global logger connected — all agent logs will stream to the UI",
|
|
3296
|
+
"system",
|
|
3297
|
+
["system", "agent"],
|
|
3298
|
+
);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// 2) Patch the runtime instance logger (if it's a different object)
|
|
3302
|
+
// This catches logs from runtime internals that use their own logger child.
|
|
3303
|
+
if (opts?.runtime?.logger && opts.runtime.logger !== logger) {
|
|
3304
|
+
if (patchLogger(opts.runtime.logger, "runtime", ["agent", "runtime"])) {
|
|
3305
|
+
addLog(
|
|
3306
|
+
"info",
|
|
3307
|
+
"Runtime logger connected — runtime logs will stream to the UI",
|
|
3308
|
+
"system",
|
|
3309
|
+
["system", "agent"],
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// Store the restart callback on the state so the route handler can access it.
|
|
3315
|
+
const onRestart = opts?.onRestart ?? null;
|
|
3316
|
+
|
|
3317
|
+
console.log(
|
|
3318
|
+
`[eliza-api] Creating http server (${Date.now() - apiStartTime}ms)`,
|
|
3319
|
+
);
|
|
3320
|
+
const server = http.createServer(async (req, res) => {
|
|
3321
|
+
try {
|
|
3322
|
+
await handleRequest(req, res, state, {
|
|
3323
|
+
onRestart,
|
|
3324
|
+
onRuntimeSwapped: () => {
|
|
3325
|
+
bindRuntimeStreams(state.runtime);
|
|
3326
|
+
void wireCoordinatorBridgesWhenReady(state, {
|
|
3327
|
+
wireChatBridge: wireCodingAgentChatBridge,
|
|
3328
|
+
wireWsBridge: wireCodingAgentWsBridge,
|
|
3329
|
+
wireEventRouting: wireCoordinatorEventRouting,
|
|
3330
|
+
wireSwarmSynthesis: wireCodingAgentSwarmSynthesis,
|
|
3331
|
+
context: "restart",
|
|
3332
|
+
logger,
|
|
3333
|
+
});
|
|
3334
|
+
},
|
|
3335
|
+
});
|
|
3336
|
+
} catch (err) {
|
|
3337
|
+
const msg = err instanceof Error ? err.message : "internal error";
|
|
3338
|
+
addLog("error", msg, "api", ["server", "api"]);
|
|
3339
|
+
error(res, msg, 500);
|
|
3340
|
+
}
|
|
3341
|
+
});
|
|
3342
|
+
console.log(`[eliza-api] Server created (${Date.now() - apiStartTime}ms)`);
|
|
3343
|
+
|
|
3344
|
+
const broadcastWs = (payload: unknown): void => {
|
|
3345
|
+
const message = JSON.stringify(payload);
|
|
3346
|
+
for (const client of wsClients) {
|
|
3347
|
+
if (client.readyState === 1) {
|
|
3348
|
+
try {
|
|
3349
|
+
client.send(message);
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
logger.error(
|
|
3352
|
+
`[eliza-api] WebSocket broadcast error: ${err instanceof Error ? err.message : err}`,
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
const pushEvent = (
|
|
3360
|
+
event: Omit<StreamEventEnvelope, "eventId" | "version">,
|
|
3361
|
+
) => {
|
|
3362
|
+
const envelope: StreamEventEnvelope = {
|
|
3363
|
+
...event,
|
|
3364
|
+
eventId: `evt-${state.nextEventId}`,
|
|
3365
|
+
version: 1,
|
|
3366
|
+
};
|
|
3367
|
+
state.nextEventId += 1;
|
|
3368
|
+
state.eventBuffer.push(envelope);
|
|
3369
|
+
if (state.eventBuffer.length > 1500) {
|
|
3370
|
+
state.eventBuffer.splice(0, state.eventBuffer.length - 1500);
|
|
3371
|
+
}
|
|
3372
|
+
broadcastWs(envelope);
|
|
3373
|
+
};
|
|
3374
|
+
|
|
3375
|
+
let detachRuntimeStreams: (() => void) | null = null;
|
|
3376
|
+
let detachTrainingStream: (() => void) | null = null;
|
|
3377
|
+
const bindRuntimeStreams = (runtime: AgentRuntime | null) => {
|
|
3378
|
+
if (detachRuntimeStreams) {
|
|
3379
|
+
detachRuntimeStreams();
|
|
3380
|
+
detachRuntimeStreams = null;
|
|
3381
|
+
}
|
|
3382
|
+
const svc = getAgentEventSvc(runtime);
|
|
3383
|
+
if (!svc) {
|
|
3384
|
+
if (runtime) {
|
|
3385
|
+
logger.warn(
|
|
3386
|
+
"[eliza-api] AGENT_EVENT service not found on runtime — event streaming will be unavailable",
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
const unsubAgentEvents = svc.subscribe((event) => {
|
|
3393
|
+
pushEvent({
|
|
3394
|
+
type: "agent_event",
|
|
3395
|
+
ts: event.ts,
|
|
3396
|
+
runId: event.runId,
|
|
3397
|
+
seq: event.seq,
|
|
3398
|
+
stream: event.stream,
|
|
3399
|
+
sessionKey: event.sessionKey,
|
|
3400
|
+
agentId: event.agentId,
|
|
3401
|
+
roomId: event.roomId,
|
|
3402
|
+
payload: event.data,
|
|
3403
|
+
});
|
|
3404
|
+
|
|
3405
|
+
void maybeRouteAutonomyEventToConversation(state, event).catch((err) => {
|
|
3406
|
+
logger.warn(
|
|
3407
|
+
`[autonomy-route] Failed to route proactive event: ${err instanceof Error ? err.message : String(err)}`,
|
|
3408
|
+
);
|
|
3409
|
+
});
|
|
3410
|
+
});
|
|
3411
|
+
|
|
3412
|
+
const unsubHeartbeat = svc.subscribeHeartbeat((event) => {
|
|
3413
|
+
pushEvent({
|
|
3414
|
+
type: "heartbeat_event",
|
|
3415
|
+
ts: event.ts,
|
|
3416
|
+
payload: event,
|
|
3417
|
+
});
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
detachRuntimeStreams = () => {
|
|
3421
|
+
unsubAgentEvents();
|
|
3422
|
+
unsubHeartbeat();
|
|
3423
|
+
};
|
|
3424
|
+
};
|
|
3425
|
+
|
|
3426
|
+
const bindTrainingStream = () => {
|
|
3427
|
+
if (detachTrainingStream) {
|
|
3428
|
+
detachTrainingStream();
|
|
3429
|
+
detachTrainingStream = null;
|
|
3430
|
+
}
|
|
3431
|
+
if (!state.trainingService) return;
|
|
3432
|
+
detachTrainingStream = state.trainingService.subscribe((event: unknown) => {
|
|
3433
|
+
const payload =
|
|
3434
|
+
typeof event === "object" && event !== null ? event : { value: event };
|
|
3435
|
+
pushEvent({
|
|
3436
|
+
type: "training_event",
|
|
3437
|
+
ts: Date.now(),
|
|
3438
|
+
payload,
|
|
3439
|
+
});
|
|
3440
|
+
});
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3443
|
+
// ── Deferred startup work (non-blocking) ────────────────────────────────
|
|
3444
|
+
// Keep API startup fast: listen first, then warm optional subsystems.
|
|
3445
|
+
const startDeferredStartupWork = () => {
|
|
3446
|
+
void (async () => {
|
|
3447
|
+
try {
|
|
3448
|
+
const discoveredSkills = await discoverSkills(
|
|
3449
|
+
workspaceDir,
|
|
3450
|
+
state.config,
|
|
3451
|
+
state.runtime,
|
|
3452
|
+
);
|
|
3453
|
+
state.skills = discoveredSkills;
|
|
3454
|
+
addLog(
|
|
3455
|
+
"info",
|
|
3456
|
+
`Discovered ${discoveredSkills.length} skills`,
|
|
3457
|
+
"system",
|
|
3458
|
+
["system", "plugins"],
|
|
3459
|
+
);
|
|
3460
|
+
} catch (err) {
|
|
3461
|
+
logger.warn(
|
|
3462
|
+
`[eliza-api] Skill discovery failed during startup: ${err instanceof Error ? err.message : String(err)}`,
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
})();
|
|
3466
|
+
|
|
3467
|
+
void (async () => {
|
|
3468
|
+
const trainingService = state.trainingService;
|
|
3469
|
+
if (!trainingService) return;
|
|
3470
|
+
try {
|
|
3471
|
+
await trainingService.initialize();
|
|
3472
|
+
bindTrainingStream();
|
|
3473
|
+
addLog("info", "Training service initialised", "system", [
|
|
3474
|
+
"system",
|
|
3475
|
+
"training",
|
|
3476
|
+
]);
|
|
3477
|
+
} catch (err) {
|
|
3478
|
+
logger.error(
|
|
3479
|
+
`[eliza-api] Training service init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
})();
|
|
3483
|
+
|
|
3484
|
+
void (async () => {
|
|
3485
|
+
initializeOGCodeInState();
|
|
3486
|
+
|
|
3487
|
+
// Get EVM private key from runtime secrets (preferred) or config.env (fallback)
|
|
3488
|
+
const runtime = state.runtime;
|
|
3489
|
+
const evmKey =
|
|
3490
|
+
(runtime?.getSetting?.("EVM_PRIVATE_KEY") as string | undefined) ??
|
|
3491
|
+
(state.config.env as Record<string, string> | undefined)
|
|
3492
|
+
?.EVM_PRIVATE_KEY;
|
|
3493
|
+
const registryConfig = state.config.registry;
|
|
3494
|
+
if (
|
|
3495
|
+
!evmKey ||
|
|
3496
|
+
!registryConfig?.registryAddress ||
|
|
3497
|
+
!registryConfig.mainnetRpc
|
|
3498
|
+
) {
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
try {
|
|
3503
|
+
const registryRpcUrl = normalizeJsonRpcUrl(registryConfig.mainnetRpc);
|
|
3504
|
+
const registryRpcProbe = await probeJsonRpcEndpoint(registryRpcUrl);
|
|
3505
|
+
if (!registryRpcProbe.ok) {
|
|
3506
|
+
addLog(
|
|
3507
|
+
"warn",
|
|
3508
|
+
`ERC-8004 registry service disabled: RPC unavailable (${registryRpcProbe.reason ?? "unknown error"})`,
|
|
3509
|
+
"system",
|
|
3510
|
+
["system"],
|
|
3511
|
+
);
|
|
3512
|
+
logger.warn(
|
|
3513
|
+
{
|
|
3514
|
+
reason: registryRpcProbe.reason,
|
|
3515
|
+
},
|
|
3516
|
+
"ERC-8004 registry service disabled because mainnetRpc is unavailable",
|
|
3517
|
+
);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
const txService = new TxService(registryRpcUrl, evmKey);
|
|
3522
|
+
state.registryService = new RegistryService(
|
|
3523
|
+
txService,
|
|
3524
|
+
registryConfig.registryAddress,
|
|
3525
|
+
);
|
|
3526
|
+
|
|
3527
|
+
if (registryConfig.collectionAddress) {
|
|
3528
|
+
const dropEnabled = state.config.features?.dropEnabled === true;
|
|
3529
|
+
state.dropService = new DropService(
|
|
3530
|
+
txService,
|
|
3531
|
+
registryConfig.collectionAddress,
|
|
3532
|
+
dropEnabled,
|
|
3533
|
+
);
|
|
3534
|
+
setElizaMakerDropService(state.dropService);
|
|
3535
|
+
} else {
|
|
3536
|
+
state.dropService = null;
|
|
3537
|
+
setElizaMakerDropService(null);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
addLog(
|
|
3541
|
+
"info",
|
|
3542
|
+
`ERC-8004 registry service initialised (${registryConfig.registryAddress})`,
|
|
3543
|
+
"system",
|
|
3544
|
+
["system"],
|
|
3545
|
+
);
|
|
3546
|
+
} catch (err) {
|
|
3547
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3548
|
+
addLog("warn", `ERC-8004 registry service disabled: ${msg}`, "system", [
|
|
3549
|
+
"system",
|
|
3550
|
+
]);
|
|
3551
|
+
logger.warn({ err }, "Failed to initialize ERC-8004 registry service");
|
|
3552
|
+
}
|
|
3553
|
+
})();
|
|
3554
|
+
|
|
3555
|
+
// ── Connector health monitoring ──────────────────────────────────────────
|
|
3556
|
+
if (state.runtime && state.config.connectors) {
|
|
3557
|
+
state.connectorHealthMonitor = new ConnectorHealthMonitor({
|
|
3558
|
+
runtime: state.runtime,
|
|
3559
|
+
config: state.config,
|
|
3560
|
+
broadcastWs,
|
|
3561
|
+
});
|
|
3562
|
+
state.connectorHealthMonitor.start();
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
// ── Dynamic streaming + connector route loading ────────────────────────
|
|
3566
|
+
// Always register generic stream routes. If a streaming destination is
|
|
3567
|
+
// configured, inject it so /api/stream/live can fetch credentials.
|
|
3568
|
+
void (async () => {
|
|
3569
|
+
try {
|
|
3570
|
+
const { handleStreamRoute } = await import("./stream-routes.js");
|
|
3571
|
+
// Screen capture manager is injected by the desktop host via globalThis
|
|
3572
|
+
const screenCapture = (globalThis as Record<string, unknown>)
|
|
3573
|
+
.__elizaScreenCapture as
|
|
3574
|
+
| {
|
|
3575
|
+
isFrameCaptureActive(): boolean;
|
|
3576
|
+
startFrameCapture(opts: {
|
|
3577
|
+
fps?: number;
|
|
3578
|
+
quality?: number;
|
|
3579
|
+
endpoint?: string;
|
|
3580
|
+
}): Promise<void>;
|
|
3581
|
+
}
|
|
3582
|
+
| undefined;
|
|
3583
|
+
|
|
3584
|
+
// Build destination registry — all configured destinations
|
|
3585
|
+
const _connectors = state.config.connectors ?? {};
|
|
3586
|
+
const streaming = (state.config as Record<string, unknown>).streaming as
|
|
3587
|
+
| Record<string, unknown>
|
|
3588
|
+
| undefined;
|
|
3589
|
+
const destinations = new Map<
|
|
3590
|
+
string,
|
|
3591
|
+
import("./stream-routes.js").StreamingDestination
|
|
3592
|
+
>();
|
|
3593
|
+
|
|
3594
|
+
// Custom RTMP
|
|
3595
|
+
if (
|
|
3596
|
+
isStreamingDestinationConfigured("customRtmp", streaming?.customRtmp)
|
|
3597
|
+
) {
|
|
3598
|
+
try {
|
|
3599
|
+
const { createCustomRtmpDestination } = await import(
|
|
3600
|
+
"../plugins/custom-rtmp/index.js"
|
|
3601
|
+
);
|
|
3602
|
+
destinations.set(
|
|
3603
|
+
"custom-rtmp",
|
|
3604
|
+
createCustomRtmpDestination(
|
|
3605
|
+
streaming?.customRtmp as {
|
|
3606
|
+
rtmpUrl?: string;
|
|
3607
|
+
rtmpKey?: string;
|
|
3608
|
+
},
|
|
3609
|
+
),
|
|
3610
|
+
);
|
|
3611
|
+
} catch (err) {
|
|
3612
|
+
logger.warn(
|
|
3613
|
+
`[eliza-api] Failed to load custom-rtmp destination: ${err instanceof Error ? err.message : String(err)}`,
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// Twitch
|
|
3619
|
+
if (isStreamingDestinationConfigured("twitch", streaming?.twitch)) {
|
|
3620
|
+
try {
|
|
3621
|
+
const twitchMod = "@elizaos/plugin-twitch-streaming";
|
|
3622
|
+
const { createTwitchDestination } = await import(twitchMod);
|
|
3623
|
+
destinations.set(
|
|
3624
|
+
"twitch",
|
|
3625
|
+
createTwitchDestination(
|
|
3626
|
+
streaming?.twitch as { streamKey?: string },
|
|
3627
|
+
),
|
|
3628
|
+
);
|
|
3629
|
+
} catch (err) {
|
|
3630
|
+
logger.warn(
|
|
3631
|
+
`[eliza-api] Failed to load twitch destination: ${err instanceof Error ? err.message : String(err)}`,
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// YouTube
|
|
3637
|
+
if (isStreamingDestinationConfigured("youtube", streaming?.youtube)) {
|
|
3638
|
+
try {
|
|
3639
|
+
const youtubeMod = "@elizaos/plugin-youtube-streaming";
|
|
3640
|
+
const { createYoutubeDestination } = await import(youtubeMod);
|
|
3641
|
+
destinations.set(
|
|
3642
|
+
"youtube",
|
|
3643
|
+
createYoutubeDestination(
|
|
3644
|
+
streaming?.youtube as { streamKey?: string; rtmpUrl?: string },
|
|
3645
|
+
),
|
|
3646
|
+
);
|
|
3647
|
+
} catch (err) {
|
|
3648
|
+
logger.warn(
|
|
3649
|
+
`[eliza-api] Failed to load youtube destination: ${err instanceof Error ? err.message : String(err)}`,
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// pump.fun
|
|
3655
|
+
if (isStreamingDestinationConfigured("pumpfun", streaming?.pumpfun)) {
|
|
3656
|
+
try {
|
|
3657
|
+
const pumpfunMod = "@elizaos/plugin-pumpfun-streaming";
|
|
3658
|
+
const { createPumpfunDestination } = await import(pumpfunMod);
|
|
3659
|
+
destinations.set(
|
|
3660
|
+
"pumpfun",
|
|
3661
|
+
createPumpfunDestination(
|
|
3662
|
+
streaming?.pumpfun as { streamKey?: string; rtmpUrl?: string },
|
|
3663
|
+
),
|
|
3664
|
+
);
|
|
3665
|
+
} catch (err) {
|
|
3666
|
+
logger.warn(
|
|
3667
|
+
`[eliza-api] Failed to load pumpfun destination: ${err instanceof Error ? err.message : String(err)}`,
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// X (Twitter)
|
|
3673
|
+
if (isStreamingDestinationConfigured("x", streaming?.x)) {
|
|
3674
|
+
try {
|
|
3675
|
+
const xMod = "@elizaos/plugin-x-streaming";
|
|
3676
|
+
const { createXStreamDestination } = await import(xMod);
|
|
3677
|
+
destinations.set(
|
|
3678
|
+
"x",
|
|
3679
|
+
createXStreamDestination(
|
|
3680
|
+
streaming?.x as { streamKey?: string; rtmpUrl?: string },
|
|
3681
|
+
),
|
|
3682
|
+
);
|
|
3683
|
+
} catch (err) {
|
|
3684
|
+
logger.warn(
|
|
3685
|
+
`[eliza-api] Failed to load x destination: ${err instanceof Error ? err.message : String(err)}`,
|
|
3686
|
+
);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// Active destination: config preference → first available
|
|
3691
|
+
const activeDestinationId =
|
|
3692
|
+
(streaming?.activeDestination as string | undefined) ??
|
|
3693
|
+
(destinations.size > 0
|
|
3694
|
+
? destinations.keys().next().value
|
|
3695
|
+
: undefined);
|
|
3696
|
+
|
|
3697
|
+
const streamState = {
|
|
3698
|
+
streamManager,
|
|
3699
|
+
port,
|
|
3700
|
+
screenCapture,
|
|
3701
|
+
captureUrl: undefined as string | undefined,
|
|
3702
|
+
destinations,
|
|
3703
|
+
activeDestinationId,
|
|
3704
|
+
activeStreamSource: { type: "stream-tab" as const },
|
|
3705
|
+
mirrorStreamAvatarToElizaConfig: (avatarIndex: number) => {
|
|
3706
|
+
try {
|
|
3707
|
+
if (!Number.isFinite(avatarIndex)) {
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
const diskCfg = loadElizaConfig();
|
|
3711
|
+
const lang = state.config.ui?.language ?? diskCfg.ui?.language;
|
|
3712
|
+
const preset = resolveStylePresetByAvatarIndex(avatarIndex, lang);
|
|
3713
|
+
const nextUi: ElizaConfig["ui"] = {
|
|
3714
|
+
...(state.config.ui ?? {}),
|
|
3715
|
+
avatarIndex,
|
|
3716
|
+
...(preset?.id ? { presetId: preset.id } : {}),
|
|
3717
|
+
};
|
|
3718
|
+
state.config = {
|
|
3719
|
+
...state.config,
|
|
3720
|
+
ui: nextUi,
|
|
3721
|
+
};
|
|
3722
|
+
// Merge disk + live server config so we never persist a minimal
|
|
3723
|
+
// snapshot (e.g. ENOENT default) and clobber eliza.json during
|
|
3724
|
+
// onboarding while state.config still holds the full boot payload.
|
|
3725
|
+
const toSave: ElizaConfig = {
|
|
3726
|
+
...diskCfg,
|
|
3727
|
+
...state.config,
|
|
3728
|
+
ui: {
|
|
3729
|
+
...(diskCfg.ui ?? {}),
|
|
3730
|
+
...(state.config.ui ?? {}),
|
|
3731
|
+
...nextUi,
|
|
3732
|
+
},
|
|
3733
|
+
};
|
|
3734
|
+
saveElizaConfig(toSave);
|
|
3735
|
+
state.config = {
|
|
3736
|
+
...state.config,
|
|
3737
|
+
ui: toSave.ui,
|
|
3738
|
+
};
|
|
3739
|
+
} catch (err) {
|
|
3740
|
+
logger.warn(
|
|
3741
|
+
`[eliza-api] mirrorStreamAvatarToElizaConfig failed: ${
|
|
3742
|
+
err instanceof Error ? err.message : String(err)
|
|
3743
|
+
}`,
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3747
|
+
get config() {
|
|
3748
|
+
const cfg = state.config as Record<string, unknown> | undefined;
|
|
3749
|
+
const msgs = cfg?.messages as Record<string, unknown> | undefined;
|
|
3750
|
+
return msgs
|
|
3751
|
+
? {
|
|
3752
|
+
messages: {
|
|
3753
|
+
tts: msgs.tts as
|
|
3754
|
+
| import("../config/types.messages.js").TtsConfig
|
|
3755
|
+
| undefined,
|
|
3756
|
+
},
|
|
3757
|
+
}
|
|
3758
|
+
: undefined;
|
|
3759
|
+
},
|
|
3760
|
+
};
|
|
3761
|
+
state.connectorRouteHandlers.push((req, res, pathname, method) =>
|
|
3762
|
+
handleStreamRoute(req, res, pathname, method, streamState),
|
|
3763
|
+
);
|
|
3764
|
+
|
|
3765
|
+
const destNames = Array.from(destinations.values())
|
|
3766
|
+
.map((d) => d.name)
|
|
3767
|
+
.join(", ");
|
|
3768
|
+
const destLabel =
|
|
3769
|
+
destinations.size > 0
|
|
3770
|
+
? `destinations: ${destNames}`
|
|
3771
|
+
: "no destinations";
|
|
3772
|
+
addLog("info", `Stream routes registered (${destLabel})`, "system", [
|
|
3773
|
+
"system",
|
|
3774
|
+
"streaming",
|
|
3775
|
+
]);
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
logger.warn(
|
|
3778
|
+
`[eliza-api] Failed to load stream routes: ${err instanceof Error ? err.message : String(err)}`,
|
|
3779
|
+
);
|
|
3780
|
+
}
|
|
3781
|
+
})();
|
|
3782
|
+
};
|
|
3783
|
+
|
|
3784
|
+
// ── WebSocket Server ─────────────────────────────────────────────────────
|
|
3785
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
3786
|
+
const wsClients = new Set<WebSocket>();
|
|
3787
|
+
const wsClientIds = new WeakMap<WebSocket, string>();
|
|
3788
|
+
/** Per-WS-client PTY output subscriptions: sessionId → unsubscribe */
|
|
3789
|
+
const wsClientPtySubscriptions = new WeakMap<
|
|
3790
|
+
WebSocket,
|
|
3791
|
+
Map<string, () => void>
|
|
3792
|
+
>();
|
|
3793
|
+
bindRuntimeStreams(opts?.runtime ?? null);
|
|
3794
|
+
bindTrainingStream();
|
|
3795
|
+
|
|
3796
|
+
// Wire coding-agent bridges at initial boot (event-driven via getServiceLoadPromise)
|
|
3797
|
+
if (opts?.runtime) {
|
|
3798
|
+
void wireCoordinatorBridgesWhenReady(state, {
|
|
3799
|
+
wireChatBridge: wireCodingAgentChatBridge,
|
|
3800
|
+
wireWsBridge: wireCodingAgentWsBridge,
|
|
3801
|
+
wireEventRouting: wireCoordinatorEventRouting,
|
|
3802
|
+
wireSwarmSynthesis: wireCodingAgentSwarmSynthesis,
|
|
3803
|
+
context: "boot",
|
|
3804
|
+
logger,
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
// Handle upgrade requests for WebSocket
|
|
3809
|
+
server.on("upgrade", (request, socket, head) => {
|
|
3810
|
+
try {
|
|
3811
|
+
const wsUrl = new URL(
|
|
3812
|
+
request.url ?? "/",
|
|
3813
|
+
`http://${request.headers.host ?? "localhost"}`,
|
|
3814
|
+
);
|
|
3815
|
+
const rejection = resolveWebSocketUpgradeRejection(request, wsUrl);
|
|
3816
|
+
if (rejection) {
|
|
3817
|
+
rejectWebSocketUpgrade(socket, rejection.status, rejection.reason);
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
|
|
3821
|
+
wss.emit("connection", ws, request);
|
|
3822
|
+
});
|
|
3823
|
+
} catch (err) {
|
|
3824
|
+
logger.error(
|
|
3825
|
+
`[eliza-api] WebSocket upgrade error: ${err instanceof Error ? err.message : err}`,
|
|
3826
|
+
);
|
|
3827
|
+
rejectWebSocketUpgrade(socket, 404, "Not found");
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
// Handle WebSocket connections
|
|
3832
|
+
wss.on("connection", (ws: WebSocket, request: http.IncomingMessage) => {
|
|
3833
|
+
let wsUrl: URL;
|
|
3834
|
+
try {
|
|
3835
|
+
wsUrl = new URL(
|
|
3836
|
+
request.url ?? "/",
|
|
3837
|
+
`http://${request.headers.host ?? "localhost"}`,
|
|
3838
|
+
);
|
|
3839
|
+
const clientId = normalizeWsClientId(wsUrl.searchParams.get("clientId"));
|
|
3840
|
+
if (clientId) wsClientIds.set(ws, clientId);
|
|
3841
|
+
} catch {
|
|
3842
|
+
// Ignore malformed WS URL metadata; auth/path were already validated.
|
|
3843
|
+
wsUrl = new URL("ws://localhost/ws");
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
let isAuthenticated = isWebSocketAuthorized(request, wsUrl);
|
|
3847
|
+
|
|
3848
|
+
const activateAuthenticatedConnection = () => {
|
|
3849
|
+
wsClients.add(ws);
|
|
3850
|
+
addLog("info", "WebSocket client connected", "websocket", [
|
|
3851
|
+
"server",
|
|
3852
|
+
"websocket",
|
|
3853
|
+
]);
|
|
3854
|
+
|
|
3855
|
+
try {
|
|
3856
|
+
ws.send(
|
|
3857
|
+
JSON.stringify({
|
|
3858
|
+
type: "status",
|
|
3859
|
+
state: state.agentState,
|
|
3860
|
+
agentName: state.agentName,
|
|
3861
|
+
model: state.model,
|
|
3862
|
+
startedAt: state.startedAt,
|
|
3863
|
+
startup: state.startup,
|
|
3864
|
+
pendingRestart: state.pendingRestartReasons.length > 0,
|
|
3865
|
+
pendingRestartReasons: state.pendingRestartReasons,
|
|
3866
|
+
}),
|
|
3867
|
+
);
|
|
3868
|
+
const replay = state.eventBuffer.slice(-120);
|
|
3869
|
+
for (const event of replay) {
|
|
3870
|
+
ws.send(JSON.stringify(event));
|
|
3871
|
+
}
|
|
3872
|
+
} catch (err) {
|
|
3873
|
+
logger.error(
|
|
3874
|
+
`[eliza-api] WebSocket send error: ${err instanceof Error ? err.message : err}`,
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
};
|
|
3878
|
+
|
|
3879
|
+
if (isAuthenticated) {
|
|
3880
|
+
activateAuthenticatedConnection();
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
ws.on("message", (data: unknown) => {
|
|
3884
|
+
try {
|
|
3885
|
+
const msg = JSON.parse(String(data));
|
|
3886
|
+
if (!isAuthenticated) {
|
|
3887
|
+
const expected = getConfiguredApiToken();
|
|
3888
|
+
if (
|
|
3889
|
+
expected &&
|
|
3890
|
+
msg.type === "auth" &&
|
|
3891
|
+
typeof msg.token === "string" &&
|
|
3892
|
+
tokenMatches(expected, msg.token.trim())
|
|
3893
|
+
) {
|
|
3894
|
+
isAuthenticated = true;
|
|
3895
|
+
ws.send(JSON.stringify({ type: "auth-ok" }));
|
|
3896
|
+
activateAuthenticatedConnection();
|
|
3897
|
+
} else {
|
|
3898
|
+
logger.warn("[eliza-api] WebSocket message rejected before auth");
|
|
3899
|
+
ws.close(1008, "Unauthorized");
|
|
3900
|
+
}
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
if (msg.type === "ping") {
|
|
3904
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
3905
|
+
} else if (msg.type === "active-conversation") {
|
|
3906
|
+
state.activeConversationId =
|
|
3907
|
+
typeof msg.conversationId === "string" ? msg.conversationId : null;
|
|
3908
|
+
} else if (
|
|
3909
|
+
msg.type === "pty-subscribe" &&
|
|
3910
|
+
typeof msg.sessionId === "string"
|
|
3911
|
+
) {
|
|
3912
|
+
const bridge = getPtyConsoleBridge(state);
|
|
3913
|
+
if (bridge) {
|
|
3914
|
+
let subs = wsClientPtySubscriptions.get(ws);
|
|
3915
|
+
if (!subs) {
|
|
3916
|
+
subs = new Map();
|
|
3917
|
+
wsClientPtySubscriptions.set(ws, subs);
|
|
3918
|
+
}
|
|
3919
|
+
// Don't double-subscribe
|
|
3920
|
+
if (!subs.has(msg.sessionId)) {
|
|
3921
|
+
const targetId = msg.sessionId;
|
|
3922
|
+
const listener = (evt: { sessionId: string; data: string }) => {
|
|
3923
|
+
if (evt.sessionId !== targetId) return;
|
|
3924
|
+
if (ws.readyState === 1) {
|
|
3925
|
+
ws.send(
|
|
3926
|
+
JSON.stringify({
|
|
3927
|
+
type: "pty-output",
|
|
3928
|
+
sessionId: targetId,
|
|
3929
|
+
data: evt.data,
|
|
3930
|
+
}),
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
};
|
|
3934
|
+
bridge.on(
|
|
3935
|
+
"session_output",
|
|
3936
|
+
listener as (...args: unknown[]) => void,
|
|
3937
|
+
);
|
|
3938
|
+
subs.set(targetId, () =>
|
|
3939
|
+
bridge.off(
|
|
3940
|
+
"session_output",
|
|
3941
|
+
listener as (...args: unknown[]) => void,
|
|
3942
|
+
),
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
} else if (
|
|
3947
|
+
msg.type === "pty-unsubscribe" &&
|
|
3948
|
+
typeof msg.sessionId === "string"
|
|
3949
|
+
) {
|
|
3950
|
+
const subs = wsClientPtySubscriptions.get(ws);
|
|
3951
|
+
const unsub = subs?.get(msg.sessionId);
|
|
3952
|
+
if (unsub) {
|
|
3953
|
+
unsub();
|
|
3954
|
+
subs?.delete(msg.sessionId);
|
|
3955
|
+
}
|
|
3956
|
+
} else if (
|
|
3957
|
+
msg.type === "pty-input" &&
|
|
3958
|
+
typeof msg.sessionId === "string" &&
|
|
3959
|
+
typeof msg.data === "string"
|
|
3960
|
+
) {
|
|
3961
|
+
// Only allow input to sessions this client has subscribed to
|
|
3962
|
+
const subs = wsClientPtySubscriptions.get(ws);
|
|
3963
|
+
if (!subs?.has(msg.sessionId)) {
|
|
3964
|
+
logger.warn(
|
|
3965
|
+
`[eliza-api] pty-input rejected: client not subscribed to session ${msg.sessionId}`,
|
|
3966
|
+
);
|
|
3967
|
+
} else if (msg.data.length > 4096) {
|
|
3968
|
+
logger.warn(
|
|
3969
|
+
`[eliza-api] pty-input rejected: payload too large (${msg.data.length} bytes) for session ${msg.sessionId}`,
|
|
3970
|
+
);
|
|
3971
|
+
} else {
|
|
3972
|
+
const bridge = getPtyConsoleBridge(state);
|
|
3973
|
+
if (bridge) {
|
|
3974
|
+
logger.debug(
|
|
3975
|
+
`[eliza-api] pty-input: session=${msg.sessionId} len=${msg.data.length}`,
|
|
3976
|
+
);
|
|
3977
|
+
bridge.writeRaw(msg.sessionId, msg.data);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
} else if (
|
|
3981
|
+
msg.type === "pty-resize" &&
|
|
3982
|
+
typeof msg.sessionId === "string"
|
|
3983
|
+
) {
|
|
3984
|
+
// Only allow resize for sessions this client has subscribed to
|
|
3985
|
+
const subs = wsClientPtySubscriptions.get(ws);
|
|
3986
|
+
if (!subs?.has(msg.sessionId)) {
|
|
3987
|
+
logger.warn(
|
|
3988
|
+
`[eliza-api] pty-resize rejected: client not subscribed to session ${msg.sessionId}`,
|
|
3989
|
+
);
|
|
3990
|
+
} else {
|
|
3991
|
+
const bridge = getPtyConsoleBridge(state);
|
|
3992
|
+
if (
|
|
3993
|
+
bridge &&
|
|
3994
|
+
typeof msg.cols === "number" &&
|
|
3995
|
+
typeof msg.rows === "number" &&
|
|
3996
|
+
Number.isFinite(msg.cols) &&
|
|
3997
|
+
Number.isFinite(msg.rows) &&
|
|
3998
|
+
Number.isInteger(msg.cols) &&
|
|
3999
|
+
Number.isInteger(msg.rows) &&
|
|
4000
|
+
msg.cols >= 1 &&
|
|
4001
|
+
msg.cols <= 500 &&
|
|
4002
|
+
msg.rows >= 1 &&
|
|
4003
|
+
msg.rows <= 500
|
|
4004
|
+
) {
|
|
4005
|
+
bridge.resize(msg.sessionId, msg.cols, msg.rows);
|
|
4006
|
+
} else {
|
|
4007
|
+
logger.warn(
|
|
4008
|
+
`[eliza-api] pty-resize rejected: invalid dimensions cols=${msg.cols} rows=${msg.rows}`,
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
} catch (err) {
|
|
4014
|
+
logger.error(
|
|
4015
|
+
`[eliza-api] WebSocket message error: ${err instanceof Error ? err.message : err}`,
|
|
4016
|
+
);
|
|
4017
|
+
}
|
|
4018
|
+
});
|
|
4019
|
+
|
|
4020
|
+
ws.on("close", () => {
|
|
4021
|
+
wsClients.delete(ws);
|
|
4022
|
+
// Clean up any PTY output subscriptions for this client
|
|
4023
|
+
const subs = wsClientPtySubscriptions.get(ws);
|
|
4024
|
+
if (subs) {
|
|
4025
|
+
for (const unsub of subs.values()) unsub();
|
|
4026
|
+
subs.clear();
|
|
4027
|
+
}
|
|
4028
|
+
addLog("info", "WebSocket client disconnected", "websocket", [
|
|
4029
|
+
"server",
|
|
4030
|
+
"websocket",
|
|
4031
|
+
]);
|
|
4032
|
+
});
|
|
4033
|
+
|
|
4034
|
+
ws.on("error", (err: unknown) => {
|
|
4035
|
+
logger.error(
|
|
4036
|
+
`[eliza-api] WebSocket error: ${err instanceof Error ? err.message : err}`,
|
|
4037
|
+
);
|
|
4038
|
+
wsClients.delete(ws);
|
|
4039
|
+
// Clean up PTY subscriptions on error too
|
|
4040
|
+
const subs = wsClientPtySubscriptions.get(ws);
|
|
4041
|
+
if (subs) {
|
|
4042
|
+
for (const unsub of subs.values()) unsub();
|
|
4043
|
+
subs.clear();
|
|
4044
|
+
}
|
|
4045
|
+
});
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
// Broadcast status to all connected WebSocket clients (flattened — PR #36 fix)
|
|
4049
|
+
const broadcastStatus = () => {
|
|
4050
|
+
broadcastWs({
|
|
4051
|
+
type: "status",
|
|
4052
|
+
state: state.agentState,
|
|
4053
|
+
agentName: state.agentName,
|
|
4054
|
+
model: state.model,
|
|
4055
|
+
startedAt: state.startedAt,
|
|
4056
|
+
startup: state.startup,
|
|
4057
|
+
pendingRestart: state.pendingRestartReasons.length > 0,
|
|
4058
|
+
pendingRestartReasons: state.pendingRestartReasons,
|
|
4059
|
+
});
|
|
4060
|
+
};
|
|
4061
|
+
|
|
4062
|
+
// Make broadcastStatus accessible to route handlers via state
|
|
4063
|
+
state.broadcastStatus = broadcastStatus;
|
|
4064
|
+
|
|
4065
|
+
// Generic broadcast — sends an arbitrary JSON payload to all WS clients.
|
|
4066
|
+
state.broadcastWs = (data: object) => {
|
|
4067
|
+
const message = JSON.stringify(data);
|
|
4068
|
+
for (const client of wsClients) {
|
|
4069
|
+
if (client.readyState === 1) {
|
|
4070
|
+
try {
|
|
4071
|
+
client.send(message);
|
|
4072
|
+
} catch (err) {
|
|
4073
|
+
logger.error(
|
|
4074
|
+
`[eliza-api] WebSocket broadcast error: ${err instanceof Error ? err.message : err}`,
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
|
|
4081
|
+
state.broadcastWsToClientId = (clientId: string, data: object) => {
|
|
4082
|
+
const message = JSON.stringify(data);
|
|
4083
|
+
let delivered = 0;
|
|
4084
|
+
for (const client of wsClients) {
|
|
4085
|
+
if (client.readyState !== 1) continue;
|
|
4086
|
+
if (wsClientIds.get(client) !== clientId) continue;
|
|
4087
|
+
try {
|
|
4088
|
+
client.send(message);
|
|
4089
|
+
delivered += 1;
|
|
4090
|
+
} catch (err) {
|
|
4091
|
+
logger.error(
|
|
4092
|
+
`[eliza-api] WebSocket targeted send error: ${err instanceof Error ? err.message : err}`,
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return delivered;
|
|
4097
|
+
};
|
|
4098
|
+
|
|
4099
|
+
// Wire up ConnectorSetupService broadcastWs so connector plugins
|
|
4100
|
+
// (Signal, WhatsApp) can broadcast pairing events via the service.
|
|
4101
|
+
if (state.runtime) {
|
|
4102
|
+
try {
|
|
4103
|
+
const setupSvc = state.runtime.getService("connector-setup") as {
|
|
4104
|
+
setBroadcastWs?: (
|
|
4105
|
+
fn: ((data: Record<string, unknown>) => void) | null,
|
|
4106
|
+
) => void;
|
|
4107
|
+
} | null;
|
|
4108
|
+
setupSvc?.setBroadcastWs?.(state.broadcastWs);
|
|
4109
|
+
} catch {
|
|
4110
|
+
// non-fatal — service may not be registered yet
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
// Broadcast status every 5 seconds
|
|
4115
|
+
const statusInterval = setInterval(broadcastStatus, 5000);
|
|
4116
|
+
|
|
4117
|
+
/**
|
|
4118
|
+
* Restore the in-memory conversation list from the database.
|
|
4119
|
+
* Web-chat rooms live in a deterministic world; we scan it for rooms
|
|
4120
|
+
* whose channelId starts with "web-conv-" and reconstruct the metadata.
|
|
4121
|
+
*/
|
|
4122
|
+
const restoreConversationsFromDb = async (
|
|
4123
|
+
rt: AgentRuntime,
|
|
4124
|
+
): Promise<void> => {
|
|
4125
|
+
try {
|
|
4126
|
+
const agentName = rt.character.name ?? "Eliza";
|
|
4127
|
+
const worldId = stringToUuid(`${agentName}-web-chat-world`);
|
|
4128
|
+
const rooms = await rt.getRoomsByWorld(worldId);
|
|
4129
|
+
if (!rooms?.length) return;
|
|
4130
|
+
|
|
4131
|
+
let restored = 0;
|
|
4132
|
+
for (const room of rooms) {
|
|
4133
|
+
// channelId is "web-conv-{uuid}" — extract the conversation id
|
|
4134
|
+
const channelId =
|
|
4135
|
+
typeof room.channelId === "string" ? room.channelId : "";
|
|
4136
|
+
if (!channelId.startsWith("web-conv-")) continue;
|
|
4137
|
+
const convId = channelId.replace("web-conv-", "");
|
|
4138
|
+
if (!convId || state.conversations.has(convId)) continue;
|
|
4139
|
+
if (state.deletedConversationIds.has(convId)) continue;
|
|
4140
|
+
|
|
4141
|
+
// Peek at the latest message to get a timestamp
|
|
4142
|
+
let updatedAt = new Date().toISOString();
|
|
4143
|
+
try {
|
|
4144
|
+
const msgs = await rt.getMemories({
|
|
4145
|
+
roomId: room.id as UUID,
|
|
4146
|
+
tableName: "messages",
|
|
4147
|
+
limit: 1,
|
|
4148
|
+
});
|
|
4149
|
+
if (msgs.length > 0 && msgs[0].createdAt) {
|
|
4150
|
+
updatedAt = new Date(msgs[0].createdAt).toISOString();
|
|
4151
|
+
}
|
|
4152
|
+
} catch {
|
|
4153
|
+
// non-fatal — use current time
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
const conversationMetadata = extractConversationMetadataFromRoom(
|
|
4157
|
+
room,
|
|
4158
|
+
convId,
|
|
4159
|
+
);
|
|
4160
|
+
|
|
4161
|
+
state.conversations.set(convId, {
|
|
4162
|
+
id: convId,
|
|
4163
|
+
title: room.name || "Chat",
|
|
4164
|
+
roomId: room.id as UUID,
|
|
4165
|
+
...(conversationMetadata ? { metadata: conversationMetadata } : {}),
|
|
4166
|
+
createdAt: updatedAt,
|
|
4167
|
+
updatedAt,
|
|
4168
|
+
});
|
|
4169
|
+
restored++;
|
|
4170
|
+
}
|
|
4171
|
+
if (restored > 0) {
|
|
4172
|
+
addLog(
|
|
4173
|
+
"info",
|
|
4174
|
+
`Restored ${restored} conversation(s) from database`,
|
|
4175
|
+
"system",
|
|
4176
|
+
["system"],
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
4179
|
+
} catch (err) {
|
|
4180
|
+
logger.warn(
|
|
4181
|
+
`[eliza-api] Failed to restore conversations from DB: ${err instanceof Error ? err.message : err}`,
|
|
4182
|
+
);
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
|
|
4186
|
+
const beginConversationRestore = (rt: AgentRuntime): Promise<void> => {
|
|
4187
|
+
const restorePromise = restoreConversationsFromDb(rt).finally(() => {
|
|
4188
|
+
if (state.conversationRestorePromise === restorePromise) {
|
|
4189
|
+
state.conversationRestorePromise = null;
|
|
4190
|
+
}
|
|
4191
|
+
});
|
|
4192
|
+
state.conversationRestorePromise = restorePromise;
|
|
4193
|
+
return restorePromise;
|
|
4194
|
+
};
|
|
4195
|
+
|
|
4196
|
+
/**
|
|
4197
|
+
* Load the agent's DB-persisted character data and overlay onto the
|
|
4198
|
+
* in-memory runtime.character. This ensures Character Editor edits
|
|
4199
|
+
* survive server restarts without depending on eliza.json persistence.
|
|
4200
|
+
*/
|
|
4201
|
+
const overlayDbCharacter = async (
|
|
4202
|
+
rt: AgentRuntime,
|
|
4203
|
+
st: typeof state,
|
|
4204
|
+
): Promise<void> => {
|
|
4205
|
+
try {
|
|
4206
|
+
const dbAgent = await rt.getAgent(rt.agentId);
|
|
4207
|
+
const agentRecord =
|
|
4208
|
+
dbAgent && typeof dbAgent === "object" && !Array.isArray(dbAgent)
|
|
4209
|
+
? Object.fromEntries(Object.entries(dbAgent))
|
|
4210
|
+
: null;
|
|
4211
|
+
const saved = agentRecord?.character as
|
|
4212
|
+
| Record<string, unknown>
|
|
4213
|
+
| undefined;
|
|
4214
|
+
if (!saved || typeof saved !== "object") return;
|
|
4215
|
+
|
|
4216
|
+
const c = rt.character;
|
|
4217
|
+
// Only overlay fields that were explicitly saved (non-empty)
|
|
4218
|
+
if (typeof saved.name === "string" && saved.name) c.name = saved.name;
|
|
4219
|
+
if (Array.isArray(saved.bio) && saved.bio.length > 0) {
|
|
4220
|
+
c.bio = saved.bio as string[];
|
|
4221
|
+
}
|
|
4222
|
+
if (typeof saved.system === "string" && saved.system) {
|
|
4223
|
+
c.system = saved.system;
|
|
4224
|
+
}
|
|
4225
|
+
if (Array.isArray(saved.adjectives)) {
|
|
4226
|
+
c.adjectives = saved.adjectives as string[];
|
|
4227
|
+
}
|
|
4228
|
+
if (Array.isArray(saved.topics)) {
|
|
4229
|
+
(c as { topics?: string[] }).topics = saved.topics as string[];
|
|
4230
|
+
}
|
|
4231
|
+
if (saved.style && typeof saved.style === "object") {
|
|
4232
|
+
c.style = saved.style as NonNullable<typeof c.style>;
|
|
4233
|
+
}
|
|
4234
|
+
if (Array.isArray(saved.messageExamples)) {
|
|
4235
|
+
c.messageExamples = saved.messageExamples as NonNullable<
|
|
4236
|
+
typeof c.messageExamples
|
|
4237
|
+
>;
|
|
4238
|
+
}
|
|
4239
|
+
if (Array.isArray(saved.postExamples) && saved.postExamples.length > 0) {
|
|
4240
|
+
c.postExamples = saved.postExamples as string[];
|
|
4241
|
+
}
|
|
4242
|
+
// Update agent name on state
|
|
4243
|
+
st.agentName = c.name ?? st.agentName;
|
|
4244
|
+
logger.info(
|
|
4245
|
+
`[character-db] Overlaid DB-persisted character "${c.name}" onto runtime`,
|
|
4246
|
+
);
|
|
4247
|
+
} catch (err) {
|
|
4248
|
+
logger.warn(
|
|
4249
|
+
`[character-db] Failed to load character from DB: ${err instanceof Error ? err.message : err}`,
|
|
4250
|
+
);
|
|
4251
|
+
}
|
|
4252
|
+
};
|
|
4253
|
+
|
|
4254
|
+
// Restore conversations from DB at initial boot (if runtime was passed in)
|
|
4255
|
+
if (opts?.runtime) {
|
|
4256
|
+
void beginConversationRestore(opts.runtime).catch((err) => {
|
|
4257
|
+
logger.warn("[api] Conversation restore failed:", err);
|
|
4258
|
+
});
|
|
4259
|
+
void overlayDbCharacter(opts.runtime, state).catch((err) => {
|
|
4260
|
+
logger.warn("[api] Character overlay restore failed:", err);
|
|
4261
|
+
});
|
|
4262
|
+
registerClientChatSendHandler(opts.runtime, state);
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
const assertX402RoutesValid = (rt: AgentRuntime | null | undefined): void => {
|
|
4266
|
+
if (!rt?.routes?.length) return;
|
|
4267
|
+
const agentId =
|
|
4268
|
+
rt.agentId != null && String(rt.agentId).length > 0
|
|
4269
|
+
? String(rt.agentId)
|
|
4270
|
+
: undefined;
|
|
4271
|
+
const result = validateX402Startup(rt.routes as Route[], rt.character, {
|
|
4272
|
+
agentId,
|
|
4273
|
+
});
|
|
4274
|
+
if (!result.valid) {
|
|
4275
|
+
throw new Error(
|
|
4276
|
+
`x402 configuration invalid:\n${result.errors.map((e) => ` • ${e}`).join("\n")}`,
|
|
4277
|
+
);
|
|
4278
|
+
}
|
|
4279
|
+
for (const w of result.warnings) {
|
|
4280
|
+
logger.warn(`[x402] ${w}`);
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
|
|
4284
|
+
/** Hot-swap the runtime reference (used after an in-process restart). */
|
|
4285
|
+
const updateRuntime = (rt: AgentRuntime): void => {
|
|
4286
|
+
assertX402RoutesValid(rt);
|
|
4287
|
+
state.runtime = rt;
|
|
4288
|
+
state.chatConnectionReady = null;
|
|
4289
|
+
state.chatConnectionPromise = null;
|
|
4290
|
+
bindRuntimeStreams(rt);
|
|
4291
|
+
// AppManager doesn't need a runtime reference
|
|
4292
|
+
state.agentState = "running";
|
|
4293
|
+
state.agentName =
|
|
4294
|
+
rt.character.name ?? resolveDefaultAgentName(state.config);
|
|
4295
|
+
state.model = detectRuntimeModel(rt, state.config);
|
|
4296
|
+
state.startedAt = Date.now();
|
|
4297
|
+
state.startup = {
|
|
4298
|
+
phase: "running",
|
|
4299
|
+
attempt: 0,
|
|
4300
|
+
};
|
|
4301
|
+
addLog("info", `Runtime restarted — agent: ${state.agentName}`, "system", [
|
|
4302
|
+
"system",
|
|
4303
|
+
"agent",
|
|
4304
|
+
]);
|
|
4305
|
+
|
|
4306
|
+
// Restore conversations from DB so they survive restarts
|
|
4307
|
+
void beginConversationRestore(rt).catch((err) => {
|
|
4308
|
+
logger.warn("[api] Conversation restore failed on restart:", err);
|
|
4309
|
+
});
|
|
4310
|
+
|
|
4311
|
+
// Overlay DB-persisted character data (from Character Editor saves)
|
|
4312
|
+
void overlayDbCharacter(rt, state).catch((err) => {
|
|
4313
|
+
logger.warn("[api] Character overlay restore failed on restart:", err);
|
|
4314
|
+
});
|
|
4315
|
+
|
|
4316
|
+
// Broadcast status update immediately after restart
|
|
4317
|
+
broadcastStatus();
|
|
4318
|
+
|
|
4319
|
+
// Re-register client_chat send handler on the new runtime
|
|
4320
|
+
registerClientChatSendHandler(rt, state);
|
|
4321
|
+
|
|
4322
|
+
// Wire coding-agent bridges (event-driven via getServiceLoadPromise)
|
|
4323
|
+
void wireCoordinatorBridgesWhenReady(state, {
|
|
4324
|
+
wireChatBridge: wireCodingAgentChatBridge,
|
|
4325
|
+
wireWsBridge: wireCodingAgentWsBridge,
|
|
4326
|
+
wireEventRouting: wireCoordinatorEventRouting,
|
|
4327
|
+
wireSwarmSynthesis: wireCodingAgentSwarmSynthesis,
|
|
4328
|
+
context: "restart",
|
|
4329
|
+
logger,
|
|
4330
|
+
});
|
|
4331
|
+
};
|
|
4332
|
+
|
|
4333
|
+
const updateStartup = (
|
|
4334
|
+
update: Partial<AgentStartupDiagnostics> & {
|
|
4335
|
+
phase?: string;
|
|
4336
|
+
attempt?: number;
|
|
4337
|
+
state?: ServerState["agentState"];
|
|
4338
|
+
},
|
|
4339
|
+
): void => {
|
|
4340
|
+
const { state: nextState, ...startupUpdate } = update;
|
|
4341
|
+
state.startup = {
|
|
4342
|
+
...state.startup,
|
|
4343
|
+
...startupUpdate,
|
|
4344
|
+
};
|
|
4345
|
+
if (nextState) {
|
|
4346
|
+
state.agentState = nextState;
|
|
4347
|
+
if (nextState === "error") {
|
|
4348
|
+
state.startedAt = undefined;
|
|
4349
|
+
} else if (
|
|
4350
|
+
(nextState === "starting" || nextState === "running") &&
|
|
4351
|
+
!state.startedAt
|
|
4352
|
+
) {
|
|
4353
|
+
state.startedAt = Date.now();
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
broadcastStatus();
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
console.log(
|
|
4360
|
+
`[eliza-api] Calling server.listen (${Date.now() - apiStartTime}ms)`,
|
|
4361
|
+
);
|
|
4362
|
+
try {
|
|
4363
|
+
assertX402RoutesValid(state.runtime);
|
|
4364
|
+
} catch (err) {
|
|
4365
|
+
return Promise.reject(err);
|
|
4366
|
+
}
|
|
4367
|
+
return new Promise((resolve, reject) => {
|
|
4368
|
+
let currentPort = port;
|
|
4369
|
+
|
|
4370
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
4371
|
+
if (err.code === "EADDRINUSE") {
|
|
4372
|
+
console.warn(
|
|
4373
|
+
`[eliza-api] Port ${currentPort} is already in use. Checking fallback...`,
|
|
4374
|
+
);
|
|
4375
|
+
if (currentPort !== 0) {
|
|
4376
|
+
console.warn(`[eliza-api] Retrying with dynamic port (0)...`);
|
|
4377
|
+
currentPort = 0;
|
|
4378
|
+
server.listen(0, host);
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
} else {
|
|
4382
|
+
console.error(
|
|
4383
|
+
`[eliza-api] Server error: ${err.message} (code: ${err.code})`,
|
|
4384
|
+
);
|
|
4385
|
+
}
|
|
4386
|
+
reject(err);
|
|
4387
|
+
});
|
|
4388
|
+
|
|
4389
|
+
server.listen(port, host, () => {
|
|
4390
|
+
console.log(
|
|
4391
|
+
`[eliza-api] server.listen callback fired (${Date.now() - apiStartTime}ms)`,
|
|
4392
|
+
);
|
|
4393
|
+
const addr = server.address();
|
|
4394
|
+
const actualPort =
|
|
4395
|
+
typeof addr === "object" && addr ? addr.port : currentPort;
|
|
4396
|
+
const displayHost =
|
|
4397
|
+
typeof addr === "object" && addr ? addr.address : host;
|
|
4398
|
+
addLog(
|
|
4399
|
+
"info",
|
|
4400
|
+
`API server listening on http://${displayHost}:${actualPort}`,
|
|
4401
|
+
"system",
|
|
4402
|
+
["server", "system"],
|
|
4403
|
+
);
|
|
4404
|
+
// Log to both stdout (for agent.ts port detection) and the in-memory
|
|
4405
|
+
// logger. agent.ts watches stdout for "Listening on http://host:PORT"
|
|
4406
|
+
// to detect dynamic port reassignment when the default port is in use.
|
|
4407
|
+
console.log(
|
|
4408
|
+
`[eliza-api] Listening on http://${displayHost}:${actualPort}`,
|
|
4409
|
+
);
|
|
4410
|
+
logger.info(
|
|
4411
|
+
`[eliza-api] Listening on http://${displayHost}:${actualPort}`,
|
|
4412
|
+
);
|
|
4413
|
+
if (!opts?.skipDeferredStartupWork) {
|
|
4414
|
+
startDeferredStartupWork();
|
|
4415
|
+
}
|
|
4416
|
+
resolve({
|
|
4417
|
+
port: actualPort,
|
|
4418
|
+
close: async () =>
|
|
4419
|
+
await new Promise<void>((r) => {
|
|
4420
|
+
void (async () => {
|
|
4421
|
+
const closeAllConnections = (
|
|
4422
|
+
server as { closeAllConnections?: () => void }
|
|
4423
|
+
).closeAllConnections;
|
|
4424
|
+
const closeIdleConnections = (
|
|
4425
|
+
server as { closeIdleConnections?: () => void }
|
|
4426
|
+
).closeIdleConnections;
|
|
4427
|
+
|
|
4428
|
+
clearInterval(statusInterval);
|
|
4429
|
+
if (state.connectorHealthMonitor) {
|
|
4430
|
+
state.connectorHealthMonitor.stop();
|
|
4431
|
+
state.connectorHealthMonitor = null;
|
|
4432
|
+
}
|
|
4433
|
+
if (detachRuntimeStreams) {
|
|
4434
|
+
detachRuntimeStreams();
|
|
4435
|
+
detachRuntimeStreams = null;
|
|
4436
|
+
}
|
|
4437
|
+
if (detachTrainingStream) {
|
|
4438
|
+
detachTrainingStream();
|
|
4439
|
+
detachTrainingStream = null;
|
|
4440
|
+
}
|
|
4441
|
+
for (const ws of wsClients) {
|
|
4442
|
+
if (ws.readyState === 1 || ws.readyState === 0) {
|
|
4443
|
+
(ws as unknown as { terminate(): void }).terminate();
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
wsClients.clear();
|
|
4447
|
+
// Clean up WhatsApp pairing sessions
|
|
4448
|
+
if (state.whatsappPairingSessions) {
|
|
4449
|
+
for (const s of state.whatsappPairingSessions.values()) {
|
|
4450
|
+
try {
|
|
4451
|
+
s.stop();
|
|
4452
|
+
} catch {
|
|
4453
|
+
/* non-fatal */
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
state.whatsappPairingSessions.clear();
|
|
4457
|
+
}
|
|
4458
|
+
// Clean up Signal pairing sessions
|
|
4459
|
+
if (state.signalPairingSessions) {
|
|
4460
|
+
for (const s of state.signalPairingSessions.values()) {
|
|
4461
|
+
try {
|
|
4462
|
+
s.stop();
|
|
4463
|
+
} catch {
|
|
4464
|
+
/* non-fatal */
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
state.signalPairingSessions.clear();
|
|
4468
|
+
}
|
|
4469
|
+
if (state.telegramAccountAuthSession) {
|
|
4470
|
+
try {
|
|
4471
|
+
await state.telegramAccountAuthSession.stop();
|
|
4472
|
+
} catch {
|
|
4473
|
+
/* non-fatal */
|
|
4474
|
+
}
|
|
4475
|
+
state.telegramAccountAuthSession = null;
|
|
4476
|
+
}
|
|
4477
|
+
wss.close();
|
|
4478
|
+
const closeTimeout = setTimeout(() => r(), 5_000);
|
|
4479
|
+
const resolved = { done: false };
|
|
4480
|
+
const finalize = () => {
|
|
4481
|
+
if (!resolved.done) {
|
|
4482
|
+
resolved.done = true;
|
|
4483
|
+
clearTimeout(closeTimeout);
|
|
4484
|
+
r();
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
if (typeof closeAllConnections === "function") {
|
|
4488
|
+
try {
|
|
4489
|
+
closeAllConnections();
|
|
4490
|
+
} catch {
|
|
4491
|
+
// Bun/Node server internals vary by runtime; non-fatal on shutdown.
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
if (typeof closeIdleConnections === "function") {
|
|
4495
|
+
try {
|
|
4496
|
+
closeIdleConnections();
|
|
4497
|
+
} catch {
|
|
4498
|
+
// Bun/Node server internals vary by runtime; non-fatal on shutdown.
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
server.close(finalize);
|
|
4502
|
+
})();
|
|
4503
|
+
}),
|
|
4504
|
+
updateRuntime,
|
|
4505
|
+
updateStartup,
|
|
4506
|
+
});
|
|
4507
|
+
});
|
|
4508
|
+
});
|
|
4509
|
+
}
|