@elizaos/plugin-registry 2.0.11-beta.7
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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/api/app-plugins-routes.d.ts +103 -0
- package/dist/api/app-plugins-routes.d.ts.map +1 -0
- package/dist/api/app-plugins-routes.js +1185 -0
- package/dist/api/app-plugins-routes.js.map +1 -0
- package/dist/api/plugin-routes.d.ts +140 -0
- package/dist/api/plugin-routes.d.ts.map +1 -0
- package/dist/api/plugin-routes.js +1391 -0
- package/dist/api/plugin-routes.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/services/plugin-installer.d.ts +22 -0
- package/dist/services/plugin-installer.d.ts.map +1 -0
- package/dist/services/plugin-installer.js +34 -0
- package/dist/services/plugin-installer.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
applyPluginRuntimeMutation,
|
|
7
|
+
CONNECTOR_ENV_MAP,
|
|
8
|
+
discoverPluginsFromManifest,
|
|
9
|
+
findPrimaryEnvKey,
|
|
10
|
+
isAdvancedCapabilityPluginId,
|
|
11
|
+
isVaultRef,
|
|
12
|
+
loadElizaConfig,
|
|
13
|
+
parseVaultRef,
|
|
14
|
+
readBundledPluginPackageMetadata,
|
|
15
|
+
resolveAdvancedCapabilitiesEnabled,
|
|
16
|
+
saveElizaConfig
|
|
17
|
+
} from "@elizaos/agent";
|
|
18
|
+
import {
|
|
19
|
+
ensureCompatSensitiveRouteAuthorized,
|
|
20
|
+
ensureRouteAuthorized
|
|
21
|
+
} from "@elizaos/app-core/api/auth";
|
|
22
|
+
import {
|
|
23
|
+
readCompatJsonBody,
|
|
24
|
+
scheduleCompatRuntimeRestart
|
|
25
|
+
} from "@elizaos/app-core/api/compat-route-shared";
|
|
26
|
+
import {
|
|
27
|
+
sendJsonError as sendJsonErrorResponse,
|
|
28
|
+
sendJson as sendJsonResponse
|
|
29
|
+
} from "@elizaos/app-core/api/response";
|
|
30
|
+
import {
|
|
31
|
+
loadRegistry
|
|
32
|
+
} from "@elizaos/app-core/registry";
|
|
33
|
+
import {
|
|
34
|
+
_resetSharedVaultForTesting,
|
|
35
|
+
mirrorPluginSensitiveToVault,
|
|
36
|
+
sharedVault
|
|
37
|
+
} from "@elizaos/app-core/services/vault-mirror";
|
|
38
|
+
import { logger } from "@elizaos/core";
|
|
39
|
+
import {
|
|
40
|
+
asRecord,
|
|
41
|
+
CONNECTOR_PLUGINS,
|
|
42
|
+
STREAMING_PLUGINS
|
|
43
|
+
} from "@elizaos/shared";
|
|
44
|
+
import { VaultMissError } from "@elizaos/vault";
|
|
45
|
+
const require2 = createRequire(import.meta.url);
|
|
46
|
+
const FIELD_TYPE_TO_LEGACY = {
|
|
47
|
+
string: "string",
|
|
48
|
+
secret: "string",
|
|
49
|
+
url: "string",
|
|
50
|
+
"file-path": "string",
|
|
51
|
+
textarea: "string",
|
|
52
|
+
json: "string",
|
|
53
|
+
select: "string",
|
|
54
|
+
multiselect: "string",
|
|
55
|
+
boolean: "boolean",
|
|
56
|
+
number: "number"
|
|
57
|
+
};
|
|
58
|
+
function pluginSubtypeToCategory(entry) {
|
|
59
|
+
if (entry.kind !== "plugin") return entry.kind;
|
|
60
|
+
if (entry.subtype === "ai-provider") return "ai-provider";
|
|
61
|
+
if (entry.subtype === "database") return "database";
|
|
62
|
+
return "feature";
|
|
63
|
+
}
|
|
64
|
+
function connectorSubtypeToCategory(entry) {
|
|
65
|
+
if (entry.kind !== "connector") return "connector";
|
|
66
|
+
if (entry.subtype === "streaming") return "streaming";
|
|
67
|
+
return "connector";
|
|
68
|
+
}
|
|
69
|
+
function categoryForRegistryEntry(entry) {
|
|
70
|
+
if (entry.kind === "plugin") return pluginSubtypeToCategory(entry);
|
|
71
|
+
if (entry.kind === "connector") return connectorSubtypeToCategory(entry);
|
|
72
|
+
return entry.kind;
|
|
73
|
+
}
|
|
74
|
+
function envKeyForRegistryEntry(entry) {
|
|
75
|
+
if (entry.kind === "connector" && entry.auth) {
|
|
76
|
+
const [first] = entry.auth.credentialKeys;
|
|
77
|
+
if (first) return first;
|
|
78
|
+
}
|
|
79
|
+
for (const [key, field] of Object.entries(entry.config)) {
|
|
80
|
+
if (field.required && (field.type === "secret" || field.sensitive)) {
|
|
81
|
+
return key;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
function fieldToManifestParameter(field) {
|
|
87
|
+
const param = {
|
|
88
|
+
type: FIELD_TYPE_TO_LEGACY[field.type],
|
|
89
|
+
description: field.help ?? field.label ?? "",
|
|
90
|
+
required: field.required,
|
|
91
|
+
sensitive: field.sensitive ?? field.type === "secret"
|
|
92
|
+
};
|
|
93
|
+
if (field.default !== void 0 && field.default !== null) {
|
|
94
|
+
param.default = String(field.default);
|
|
95
|
+
}
|
|
96
|
+
if (field.options) {
|
|
97
|
+
param.options = field.options.map((option) => option.value);
|
|
98
|
+
}
|
|
99
|
+
return param;
|
|
100
|
+
}
|
|
101
|
+
function registryEntryToManifest(entry) {
|
|
102
|
+
const pluginParameters = {};
|
|
103
|
+
for (const [key, field] of Object.entries(entry.config)) {
|
|
104
|
+
pluginParameters[key] = fieldToManifestParameter(field);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: entry.id,
|
|
108
|
+
dirName: entry.npmName?.replace(/^@[^/]+\//, ""),
|
|
109
|
+
name: entry.name,
|
|
110
|
+
npmName: entry.npmName,
|
|
111
|
+
description: entry.description,
|
|
112
|
+
tags: entry.tags,
|
|
113
|
+
category: categoryForRegistryEntry(entry),
|
|
114
|
+
envKey: envKeyForRegistryEntry(entry),
|
|
115
|
+
configKeys: Object.keys(entry.config),
|
|
116
|
+
version: entry.version,
|
|
117
|
+
pluginParameters,
|
|
118
|
+
icon: entry.render.icon ?? null,
|
|
119
|
+
homepage: entry.resources.homepage,
|
|
120
|
+
repository: entry.resources.repository,
|
|
121
|
+
setupGuideUrl: entry.resources.setupGuideUrl
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const CAPABILITY_FEATURE_IDS = /* @__PURE__ */ new Set([
|
|
125
|
+
"vision",
|
|
126
|
+
"browser",
|
|
127
|
+
"computeruse",
|
|
128
|
+
"coding-agent"
|
|
129
|
+
]);
|
|
130
|
+
const ADVANCED_CAPABILITY_SERVICE_BY_PLUGIN_ID = {
|
|
131
|
+
experience: "EXPERIENCE",
|
|
132
|
+
personality: "CHARACTER_MANAGEMENT"
|
|
133
|
+
};
|
|
134
|
+
const SENSITIVE_KEY_PREFIXES = ["SOLANA_", "ETHEREUM_", "EVM_", "WALLET_"];
|
|
135
|
+
const REVEALABLE_KEY_PREFIXES = [
|
|
136
|
+
"OPENAI_",
|
|
137
|
+
"ANTHROPIC_",
|
|
138
|
+
"GOOGLE_",
|
|
139
|
+
"GROQ_",
|
|
140
|
+
"MISTRAL_",
|
|
141
|
+
"PERPLEXITY_",
|
|
142
|
+
"COHERE_",
|
|
143
|
+
"TOGETHER_",
|
|
144
|
+
"FIREWORKS_",
|
|
145
|
+
"REPLICATE_",
|
|
146
|
+
"HUGGINGFACE_",
|
|
147
|
+
"ELEVENLABS_",
|
|
148
|
+
"DISCORD_",
|
|
149
|
+
"TELEGRAM_",
|
|
150
|
+
"TWITTER_",
|
|
151
|
+
"SLACK_",
|
|
152
|
+
"GITHUB_",
|
|
153
|
+
"REDIS_",
|
|
154
|
+
"POSTGRES_",
|
|
155
|
+
"DATABASE_",
|
|
156
|
+
"SUPABASE_",
|
|
157
|
+
"PINECONE_",
|
|
158
|
+
"QDRANT_",
|
|
159
|
+
"WEAVIATE_",
|
|
160
|
+
"CHROMADB_",
|
|
161
|
+
"AWS_",
|
|
162
|
+
"AZURE_",
|
|
163
|
+
"CLOUDFLARE_",
|
|
164
|
+
"ELIZA_",
|
|
165
|
+
"PLUGIN_",
|
|
166
|
+
"XAI_",
|
|
167
|
+
"DEEPSEEK_",
|
|
168
|
+
"OLLAMA_",
|
|
169
|
+
"FAL_",
|
|
170
|
+
"LETZAI_",
|
|
171
|
+
"GAIANET_",
|
|
172
|
+
"LIVEPEER_",
|
|
173
|
+
...SENSITIVE_KEY_PREFIXES
|
|
174
|
+
];
|
|
175
|
+
const DRIFT_LOG_THROTTLE_MS = 5 * 60 * 1e3;
|
|
176
|
+
let _lastDriftWarningAt = 0;
|
|
177
|
+
let _lastDriftWarningFingerprint = "";
|
|
178
|
+
function maskValue(value) {
|
|
179
|
+
if (value.length <= 8) return "****";
|
|
180
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
181
|
+
}
|
|
182
|
+
function normalizePluginCategory(value) {
|
|
183
|
+
switch (value) {
|
|
184
|
+
case "ai-provider":
|
|
185
|
+
case "connector":
|
|
186
|
+
case "streaming":
|
|
187
|
+
case "database":
|
|
188
|
+
case "app":
|
|
189
|
+
return value;
|
|
190
|
+
default:
|
|
191
|
+
return "feature";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function normalizePluginId(rawName) {
|
|
195
|
+
const scopedPackage = rawName.match(/^@[^/]+\/(?:plugin|app)-(.+)$/);
|
|
196
|
+
if (scopedPackage) {
|
|
197
|
+
return scopedPackage[1] ?? rawName;
|
|
198
|
+
}
|
|
199
|
+
return rawName.replace(/^@[^/]+\//, "").replace(/^(plugin|app)-/, "");
|
|
200
|
+
}
|
|
201
|
+
function decodePluginPathSegment(rawSegment) {
|
|
202
|
+
try {
|
|
203
|
+
return decodeURIComponent(rawSegment);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function resolveCompatConfigKey(pluginId, npmName, pluginMap) {
|
|
209
|
+
const candidates = /* @__PURE__ */ new Set([pluginId, normalizePluginId(pluginId)]);
|
|
210
|
+
if (typeof npmName === "string" && npmName.length > 0) {
|
|
211
|
+
candidates.add(npmName);
|
|
212
|
+
candidates.add(normalizePluginId(npmName));
|
|
213
|
+
}
|
|
214
|
+
for (const [configKey, packageName] of Object.entries(pluginMap)) {
|
|
215
|
+
if (candidates.has(configKey) || candidates.has(packageName) || candidates.has(normalizePluginId(packageName))) {
|
|
216
|
+
return configKey;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function readCompatSectionEnabled(section, configKey) {
|
|
222
|
+
if (!configKey) {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
const sectionRecord = asRecord(section);
|
|
226
|
+
if (!sectionRecord) {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
const targetRecord = asRecord(sectionRecord[configKey]);
|
|
230
|
+
if (!targetRecord || typeof targetRecord.enabled !== "boolean") {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
return targetRecord.enabled;
|
|
234
|
+
}
|
|
235
|
+
function writeCompatSectionEnabled(parent, sectionKey, configKey, enabled) {
|
|
236
|
+
if (!configKey) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const section = asRecord(parent[sectionKey]) ?? {};
|
|
240
|
+
const entry = asRecord(section[configKey]) ?? {};
|
|
241
|
+
entry.enabled = enabled;
|
|
242
|
+
section[configKey] = entry;
|
|
243
|
+
parent[sectionKey] = section;
|
|
244
|
+
}
|
|
245
|
+
function syncCompatConnectorConfigValues(config, pluginId, npmName, values) {
|
|
246
|
+
const connectorKey = resolveCompatConfigKey(
|
|
247
|
+
pluginId,
|
|
248
|
+
npmName,
|
|
249
|
+
CONNECTOR_PLUGINS
|
|
250
|
+
);
|
|
251
|
+
if (!connectorKey) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const envMap = CONNECTOR_ENV_MAP[connectorKey];
|
|
255
|
+
if (!envMap) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const typedEnvMap = envMap;
|
|
259
|
+
const connectors = asRecord(config.connectors) ?? {};
|
|
260
|
+
const connectorEntry = asRecord(connectors[connectorKey]) ?? {};
|
|
261
|
+
const envToField = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const [field, envKey] of Object.entries(typedEnvMap)) {
|
|
263
|
+
if (!envToField.has(envKey)) {
|
|
264
|
+
envToField.set(envKey, field);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
let touched = false;
|
|
268
|
+
for (const [envKey, field] of envToField.entries()) {
|
|
269
|
+
if (!(envKey in values)) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
touched = true;
|
|
273
|
+
const value = values[envKey];
|
|
274
|
+
if (value.trim()) {
|
|
275
|
+
connectorEntry[field] = value;
|
|
276
|
+
} else {
|
|
277
|
+
delete connectorEntry[field];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (connectorKey === "discord" && "DISCORD_API_TOKEN" in values) {
|
|
281
|
+
touched = true;
|
|
282
|
+
const tokenValue = values.DISCORD_API_TOKEN.trim();
|
|
283
|
+
if (tokenValue) {
|
|
284
|
+
connectorEntry.token = tokenValue;
|
|
285
|
+
} else {
|
|
286
|
+
delete connectorEntry.token;
|
|
287
|
+
}
|
|
288
|
+
delete connectorEntry.botToken;
|
|
289
|
+
}
|
|
290
|
+
if (!touched) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
connectors[connectorKey] = connectorEntry;
|
|
294
|
+
config.connectors = connectors;
|
|
295
|
+
}
|
|
296
|
+
function resolvePersistedPluginEnabled(pluginId, category, npmName, configEntries, config) {
|
|
297
|
+
const pluginEnabled = typeof configEntries[pluginId]?.enabled === "boolean" ? Boolean(configEntries[pluginId]?.enabled) : void 0;
|
|
298
|
+
if (category === "connector") {
|
|
299
|
+
const connectorEnabled = readCompatSectionEnabled(
|
|
300
|
+
config.connectors,
|
|
301
|
+
resolveCompatConfigKey(pluginId, npmName, CONNECTOR_PLUGINS)
|
|
302
|
+
);
|
|
303
|
+
return connectorEnabled ?? pluginEnabled;
|
|
304
|
+
}
|
|
305
|
+
if (category === "streaming") {
|
|
306
|
+
const streamingEnabled = readCompatSectionEnabled(
|
|
307
|
+
config.streaming,
|
|
308
|
+
resolveCompatConfigKey(pluginId, npmName, STREAMING_PLUGINS)
|
|
309
|
+
);
|
|
310
|
+
return streamingEnabled ?? pluginEnabled;
|
|
311
|
+
}
|
|
312
|
+
return pluginEnabled;
|
|
313
|
+
}
|
|
314
|
+
function resolveCompatPluginEnabledForList(active, persistedEnabled, advancedCapabilityEnabled) {
|
|
315
|
+
return advancedCapabilityEnabled ?? persistedEnabled ?? active;
|
|
316
|
+
}
|
|
317
|
+
function shortPluginIdFromNpmName(npmName) {
|
|
318
|
+
if (!npmName || typeof npmName !== "string") {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
if (npmName.startsWith("@elizaos/app-")) {
|
|
322
|
+
return npmName.slice("@elizaos/".length);
|
|
323
|
+
}
|
|
324
|
+
if (npmName.startsWith("@elizaos/plugin-")) {
|
|
325
|
+
return npmName.slice("@elizaos/plugin-".length);
|
|
326
|
+
}
|
|
327
|
+
return normalizePluginId(npmName);
|
|
328
|
+
}
|
|
329
|
+
function analyzePluginStateDrift(pluginList, configRecord, configEntries, allowList) {
|
|
330
|
+
const diagnostics = pluginList.map((plugin) => {
|
|
331
|
+
const pluginId = String(plugin.id);
|
|
332
|
+
const category = normalizePluginCategory(plugin.category);
|
|
333
|
+
const npmName = typeof plugin.npmName === "string" && plugin.npmName.length > 0 ? plugin.npmName : null;
|
|
334
|
+
const shortId = shortPluginIdFromNpmName(npmName) ?? pluginId;
|
|
335
|
+
const uiEnabled = Boolean(plugin.enabled);
|
|
336
|
+
const compatEnabled = category === "connector" ? readCompatSectionEnabled(
|
|
337
|
+
configRecord.connectors,
|
|
338
|
+
resolveCompatConfigKey(
|
|
339
|
+
pluginId,
|
|
340
|
+
npmName ?? void 0,
|
|
341
|
+
CONNECTOR_PLUGINS
|
|
342
|
+
)
|
|
343
|
+
) : category === "streaming" ? readCompatSectionEnabled(
|
|
344
|
+
configRecord.streaming,
|
|
345
|
+
resolveCompatConfigKey(
|
|
346
|
+
pluginId,
|
|
347
|
+
npmName ?? void 0,
|
|
348
|
+
STREAMING_PLUGINS
|
|
349
|
+
)
|
|
350
|
+
) : void 0;
|
|
351
|
+
const entryEnabled = typeof configEntries[pluginId]?.enabled === "boolean" ? Boolean(configEntries[pluginId]?.enabled) : void 0;
|
|
352
|
+
const enabledAllowList = allowList === null || npmName == null ? null : allowList.has(npmName) || allowList.has(shortId);
|
|
353
|
+
const isActive = Boolean(plugin.isActive);
|
|
354
|
+
const driftFlags = [];
|
|
355
|
+
if (compatEnabled !== void 0 && entryEnabled !== void 0 && compatEnabled !== entryEnabled) {
|
|
356
|
+
driftFlags.push("entries_vs_compat");
|
|
357
|
+
}
|
|
358
|
+
if (enabledAllowList !== null && entryEnabled !== void 0) {
|
|
359
|
+
if (enabledAllowList !== entryEnabled) {
|
|
360
|
+
driftFlags.push("entries_vs_allowlist");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (uiEnabled && !isActive) {
|
|
364
|
+
driftFlags.push("inactive_but_enabled");
|
|
365
|
+
}
|
|
366
|
+
if (!uiEnabled && isActive) {
|
|
367
|
+
driftFlags.push("active_but_disabled");
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
pluginId,
|
|
371
|
+
npmName,
|
|
372
|
+
category,
|
|
373
|
+
enabled_ui: uiEnabled,
|
|
374
|
+
enabled_allowlist: enabledAllowList,
|
|
375
|
+
is_active: isActive,
|
|
376
|
+
drift_flags: driftFlags
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
const withDrift = diagnostics.filter(
|
|
380
|
+
(plugin) => plugin.drift_flags.length > 0
|
|
381
|
+
);
|
|
382
|
+
const byFlag = {
|
|
383
|
+
entries_vs_compat: 0,
|
|
384
|
+
entries_vs_allowlist: 0,
|
|
385
|
+
inactive_but_enabled: 0,
|
|
386
|
+
active_but_disabled: 0
|
|
387
|
+
};
|
|
388
|
+
for (const plugin of withDrift) {
|
|
389
|
+
for (const flag of plugin.drift_flags) {
|
|
390
|
+
byFlag[flag] += 1;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
summary: {
|
|
395
|
+
total: diagnostics.length,
|
|
396
|
+
withDrift: withDrift.length,
|
|
397
|
+
byFlag
|
|
398
|
+
},
|
|
399
|
+
plugins: diagnostics
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function buildPluginDriftDiagnostics(runtime) {
|
|
403
|
+
const pluginList = buildPluginListResponse(runtime).plugins;
|
|
404
|
+
const config = loadElizaConfig();
|
|
405
|
+
const configRecord = config;
|
|
406
|
+
const configEntries = config.plugins?.entries ?? {};
|
|
407
|
+
const allowList = Array.isArray(config.plugins?.allow) ? new Set(config.plugins.allow) : null;
|
|
408
|
+
return analyzePluginStateDrift(
|
|
409
|
+
pluginList,
|
|
410
|
+
configRecord,
|
|
411
|
+
configEntries,
|
|
412
|
+
allowList
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function maybeLogPluginStateDrift(report) {
|
|
416
|
+
if (report.summary.withDrift === 0) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const drifted = report.plugins.filter((plugin) => plugin.drift_flags.length > 0).map((plugin) => `${plugin.pluginId}:${plugin.drift_flags.join("+")}`).sort();
|
|
420
|
+
const fingerprint = drifted.join("|");
|
|
421
|
+
const now = Date.now();
|
|
422
|
+
if (fingerprint === _lastDriftWarningFingerprint && now - _lastDriftWarningAt < DRIFT_LOG_THROTTLE_MS) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
_lastDriftWarningAt = now;
|
|
426
|
+
_lastDriftWarningFingerprint = fingerprint;
|
|
427
|
+
logger.warn(
|
|
428
|
+
{
|
|
429
|
+
src: "api:plugins",
|
|
430
|
+
driftCount: report.summary.withDrift,
|
|
431
|
+
byFlag: report.summary.byFlag,
|
|
432
|
+
plugins: drifted
|
|
433
|
+
},
|
|
434
|
+
"Plugin enable-state drift detected between /api/plugins and /api/plugins/core models"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
let _enabledStateReconciled = false;
|
|
438
|
+
function reconcilePluginEnabledStates() {
|
|
439
|
+
if (_enabledStateReconciled) return;
|
|
440
|
+
_enabledStateReconciled = true;
|
|
441
|
+
const config = loadElizaConfig();
|
|
442
|
+
const configRecord = config;
|
|
443
|
+
const entries = config.plugins?.entries ?? {};
|
|
444
|
+
let dirty = false;
|
|
445
|
+
for (const [pluginId, entry] of Object.entries(entries)) {
|
|
446
|
+
if (typeof entry.enabled !== "boolean") continue;
|
|
447
|
+
const connectorKey = resolveCompatConfigKey(
|
|
448
|
+
pluginId,
|
|
449
|
+
void 0,
|
|
450
|
+
CONNECTOR_PLUGINS
|
|
451
|
+
);
|
|
452
|
+
if (connectorKey) {
|
|
453
|
+
const sectionEnabled = readCompatSectionEnabled(
|
|
454
|
+
configRecord.connectors,
|
|
455
|
+
connectorKey
|
|
456
|
+
);
|
|
457
|
+
if (sectionEnabled !== void 0 && sectionEnabled !== entry.enabled) {
|
|
458
|
+
writeCompatSectionEnabled(
|
|
459
|
+
configRecord,
|
|
460
|
+
"connectors",
|
|
461
|
+
connectorKey,
|
|
462
|
+
entry.enabled
|
|
463
|
+
);
|
|
464
|
+
dirty = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const streamingKey = resolveCompatConfigKey(
|
|
468
|
+
pluginId,
|
|
469
|
+
void 0,
|
|
470
|
+
STREAMING_PLUGINS
|
|
471
|
+
);
|
|
472
|
+
if (streamingKey) {
|
|
473
|
+
const sectionEnabled = readCompatSectionEnabled(
|
|
474
|
+
configRecord.streaming,
|
|
475
|
+
streamingKey
|
|
476
|
+
);
|
|
477
|
+
if (sectionEnabled !== void 0 && sectionEnabled !== entry.enabled) {
|
|
478
|
+
writeCompatSectionEnabled(
|
|
479
|
+
configRecord,
|
|
480
|
+
"streaming",
|
|
481
|
+
streamingKey,
|
|
482
|
+
entry.enabled
|
|
483
|
+
);
|
|
484
|
+
dirty = true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (dirty) {
|
|
489
|
+
saveElizaConfig(config);
|
|
490
|
+
logger.info("[plugins] Reconciled drifted plugin enabled states in config");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function compatMutationRequiresRestart(plugin, body) {
|
|
494
|
+
if (typeof body.enabled === "boolean") {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (body.config !== void 0 && (plugin.category === "connector" || plugin.category === "streaming")) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
function createCompatRuntimeApplyFallback(reason, requiresRestart) {
|
|
503
|
+
return {
|
|
504
|
+
mode: requiresRestart ? "restart_required" : "none",
|
|
505
|
+
requiresRestart,
|
|
506
|
+
restartedRuntime: false,
|
|
507
|
+
loadedPackages: [],
|
|
508
|
+
unloadedPackages: [],
|
|
509
|
+
reloadedPackages: [],
|
|
510
|
+
appliedConfigPackage: null,
|
|
511
|
+
reason
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function applyCompatRuntimeMutation(options) {
|
|
515
|
+
const { state, pluginId, plugin, body, previousConfig, nextConfig } = options;
|
|
516
|
+
const reason = typeof body.enabled === "boolean" ? `Plugin toggle: ${pluginId}` : `Plugin config updated: ${pluginId}`;
|
|
517
|
+
const requiresRestartFallback = compatMutationRequiresRestart(plugin, body);
|
|
518
|
+
if (!state.current) {
|
|
519
|
+
return createCompatRuntimeApplyFallback(reason, requiresRestartFallback);
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
return await applyPluginRuntimeMutation({
|
|
523
|
+
runtime: state.current,
|
|
524
|
+
previousConfig,
|
|
525
|
+
nextConfig,
|
|
526
|
+
changedPluginId: pluginId,
|
|
527
|
+
changedPluginPackage: plugin.npmName,
|
|
528
|
+
config: body.config && typeof body.config === "object" && !Array.isArray(body.config) ? body.config : void 0,
|
|
529
|
+
expectRuntimeGraphChange: typeof body.enabled === "boolean",
|
|
530
|
+
reason
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
logger.warn(
|
|
534
|
+
`[api/plugins] Live runtime apply failed for "${pluginId}": ${error instanceof Error ? error.message : String(error)}`
|
|
535
|
+
);
|
|
536
|
+
return createCompatRuntimeApplyFallback(reason, true);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function titleCasePluginId(id) {
|
|
540
|
+
return id.split("-").filter((segment) => segment.length > 0).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
541
|
+
}
|
|
542
|
+
function inferSensitiveConfigKey(key) {
|
|
543
|
+
return /(?:_API_KEY|_SECRET|_TOKEN|_PASSWORD|_PRIVATE_KEY|_SIGNING_|ENCRYPTION_)/i.test(
|
|
544
|
+
key
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
function buildPluginParamDefs(parameters, savedValues) {
|
|
548
|
+
if (!parameters) {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
const allKeys = Object.keys(parameters);
|
|
552
|
+
const GENERIC_FALLBACK_SUFFIXES = [
|
|
553
|
+
"SMALL_MODEL",
|
|
554
|
+
"LARGE_MODEL",
|
|
555
|
+
"IMAGE_MODEL",
|
|
556
|
+
"EMBEDDING_MODEL"
|
|
557
|
+
];
|
|
558
|
+
const filteredEntries = Object.entries(parameters).filter(([key]) => {
|
|
559
|
+
if (!GENERIC_FALLBACK_SUFFIXES.includes(key)) return true;
|
|
560
|
+
return !allKeys.some((other) => other !== key && other.endsWith(`_${key}`));
|
|
561
|
+
});
|
|
562
|
+
return filteredEntries.map(([key, definition]) => {
|
|
563
|
+
const envValue = process.env[key]?.trim() || void 0;
|
|
564
|
+
const savedValue = savedValues?.[key];
|
|
565
|
+
const effectiveValue = envValue ?? (savedValue ? savedValue.trim() || void 0 : void 0);
|
|
566
|
+
const isSet = Boolean(effectiveValue);
|
|
567
|
+
const sensitive = typeof definition.sensitive === "boolean" ? definition.sensitive : inferSensitiveConfigKey(key);
|
|
568
|
+
const currentValue = !isSet || !effectiveValue ? null : sensitive ? maskValue(effectiveValue) : effectiveValue;
|
|
569
|
+
return {
|
|
570
|
+
key,
|
|
571
|
+
type: definition.type ?? "string",
|
|
572
|
+
description: definition.description ?? "",
|
|
573
|
+
required: definition.required === true || definition.optional === false && definition.required !== false,
|
|
574
|
+
sensitive,
|
|
575
|
+
default: definition.default === void 0 ? void 0 : String(definition.default),
|
|
576
|
+
options: Array.isArray(definition.options) ? definition.options : void 0,
|
|
577
|
+
currentValue,
|
|
578
|
+
isSet
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function findNearestFile(startDir, fileName, maxDepth = 12) {
|
|
583
|
+
let dir = path.resolve(startDir);
|
|
584
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
585
|
+
const candidate = path.join(dir, fileName);
|
|
586
|
+
if (fs.existsSync(candidate)) {
|
|
587
|
+
return candidate;
|
|
588
|
+
}
|
|
589
|
+
const parent = path.dirname(dir);
|
|
590
|
+
if (parent === dir) {
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
dir = parent;
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
function resolvePluginManifestPath() {
|
|
598
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
599
|
+
const candidates = [
|
|
600
|
+
process.cwd(),
|
|
601
|
+
moduleDir,
|
|
602
|
+
path.dirname(process.execPath),
|
|
603
|
+
path.join(path.dirname(process.execPath), "..", "Resources", "app")
|
|
604
|
+
];
|
|
605
|
+
for (const candidate of candidates) {
|
|
606
|
+
const manifestPath = findNearestFile(candidate, "plugins.json");
|
|
607
|
+
if (manifestPath) {
|
|
608
|
+
return manifestPath;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function resolveInstalledPackageVersion(packageName) {
|
|
614
|
+
if (!packageName) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
const packageJsonPath = require2.resolve(`${packageName}/package.json`);
|
|
619
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
620
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function resolveLoadedPluginNames(runtime) {
|
|
626
|
+
const loadedNames = /* @__PURE__ */ new Set();
|
|
627
|
+
for (const plugin of runtime?.plugins ?? []) {
|
|
628
|
+
const name = plugin.name;
|
|
629
|
+
if (typeof name === "string" && name.length > 0) {
|
|
630
|
+
loadedNames.add(name);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return loadedNames;
|
|
634
|
+
}
|
|
635
|
+
function isPluginLoaded(pluginId, npmName, loadedNames) {
|
|
636
|
+
const expectedNames = /* @__PURE__ */ new Set([
|
|
637
|
+
pluginId,
|
|
638
|
+
`plugin-${pluginId}`,
|
|
639
|
+
`app-${pluginId}`,
|
|
640
|
+
npmName ?? ""
|
|
641
|
+
]);
|
|
642
|
+
for (const loadedName of loadedNames) {
|
|
643
|
+
if (expectedNames.has(loadedName)) {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
if (loadedName.endsWith(`/plugin-${pluginId}`) || loadedName.endsWith(`/app-${pluginId}`)) {
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
function resolveAdvancedCapabilityCompatStatus(pluginId, config, runtime) {
|
|
653
|
+
if (!isAdvancedCapabilityPluginId(pluginId)) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
const enabled = resolveAdvancedCapabilitiesEnabled(config);
|
|
657
|
+
if (!enabled) {
|
|
658
|
+
return { enabled: false, isActive: false };
|
|
659
|
+
}
|
|
660
|
+
const serviceType = ADVANCED_CAPABILITY_SERVICE_BY_PLUGIN_ID[pluginId];
|
|
661
|
+
return {
|
|
662
|
+
enabled: true,
|
|
663
|
+
isActive: serviceType ? Boolean(runtime?.getService(serviceType)) : Boolean(runtime)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function buildPluginListResponse(runtime) {
|
|
667
|
+
reconcilePluginEnabledStates();
|
|
668
|
+
const config = loadElizaConfig();
|
|
669
|
+
const configRecord = config;
|
|
670
|
+
const loadedNames = resolveLoadedPluginNames(runtime);
|
|
671
|
+
const registry = loadRegistry();
|
|
672
|
+
const manifestRoot = resolvePluginManifestPath() ? path.dirname(resolvePluginManifestPath() ?? "") : process.cwd();
|
|
673
|
+
const manifest = {
|
|
674
|
+
plugins: registry.all.map(registryEntryToManifest)
|
|
675
|
+
};
|
|
676
|
+
const configEntries = config.plugins?.entries ?? {};
|
|
677
|
+
const installEntries = config.plugins?.installs ?? {};
|
|
678
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
679
|
+
for (const entry of manifest.plugins ?? []) {
|
|
680
|
+
const pluginId = normalizePluginId(entry.id);
|
|
681
|
+
const category = normalizePluginCategory(entry.category);
|
|
682
|
+
const bundledMeta = entry.dirName && manifestRoot ? readBundledPluginPackageMetadata(
|
|
683
|
+
manifestRoot,
|
|
684
|
+
entry.dirName,
|
|
685
|
+
entry.npmName
|
|
686
|
+
) : void 0;
|
|
687
|
+
const configKeys = Array.isArray(entry.configKeys) && entry.configKeys.length > 0 ? entry.configKeys : bundledMeta?.configKeys ?? [];
|
|
688
|
+
const envKey = entry.envKey ?? findPrimaryEnvKey(configKeys);
|
|
689
|
+
const parameters = buildPluginParamDefs(
|
|
690
|
+
entry.pluginParameters ?? bundledMeta?.pluginParameters
|
|
691
|
+
);
|
|
692
|
+
const advancedCapabilityStatus = resolveAdvancedCapabilityCompatStatus(
|
|
693
|
+
pluginId,
|
|
694
|
+
config,
|
|
695
|
+
runtime
|
|
696
|
+
);
|
|
697
|
+
const active = advancedCapabilityStatus?.isActive ?? isPluginLoaded(pluginId, entry.npmName, loadedNames);
|
|
698
|
+
const persistedEnabled = resolvePersistedPluginEnabled(
|
|
699
|
+
pluginId,
|
|
700
|
+
category,
|
|
701
|
+
entry.npmName,
|
|
702
|
+
configEntries,
|
|
703
|
+
configRecord
|
|
704
|
+
);
|
|
705
|
+
const enabled = resolveCompatPluginEnabledForList(
|
|
706
|
+
active,
|
|
707
|
+
persistedEnabled,
|
|
708
|
+
advancedCapabilityStatus?.enabled
|
|
709
|
+
);
|
|
710
|
+
const validationErrors = parameters.filter((parameter) => parameter.required && !parameter.isSet).map((parameter) => ({
|
|
711
|
+
field: parameter.key,
|
|
712
|
+
message: "Required value is not configured."
|
|
713
|
+
}));
|
|
714
|
+
const registryEntry = registry.byId.get(pluginId);
|
|
715
|
+
plugins.set(pluginId, {
|
|
716
|
+
id: pluginId,
|
|
717
|
+
name: entry.name ?? titleCasePluginId(pluginId),
|
|
718
|
+
description: entry.description ?? bundledMeta?.description ?? "",
|
|
719
|
+
tags: entry.tags ?? [],
|
|
720
|
+
enabled,
|
|
721
|
+
configured: validationErrors.length === 0,
|
|
722
|
+
envKey,
|
|
723
|
+
category,
|
|
724
|
+
source: "bundled",
|
|
725
|
+
configKeys,
|
|
726
|
+
parameters,
|
|
727
|
+
validationErrors,
|
|
728
|
+
validationWarnings: [],
|
|
729
|
+
npmName: entry.npmName,
|
|
730
|
+
version: resolveInstalledPackageVersion(entry.npmName) ?? entry.version ?? void 0,
|
|
731
|
+
pluginDeps: entry.pluginDeps,
|
|
732
|
+
isActive: active,
|
|
733
|
+
configUiHints: entry.configUiHints ?? bundledMeta?.configUiHints,
|
|
734
|
+
icon: entry.logoUrl ?? bundledMeta?.icon ?? null,
|
|
735
|
+
homepage: entry.homepage ?? bundledMeta?.homepage,
|
|
736
|
+
repository: entry.repository ?? bundledMeta?.repository,
|
|
737
|
+
setupGuideUrl: entry.setupGuideUrl,
|
|
738
|
+
iconName: registryEntry?.render.icon,
|
|
739
|
+
group: registryEntry?.render.group,
|
|
740
|
+
groupOrder: registryEntry?.render.groupOrder,
|
|
741
|
+
visible: registryEntry?.render.visible ?? true
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
for (const entry of discoverPluginsFromManifest()) {
|
|
745
|
+
const pluginId = normalizePluginId(entry.id);
|
|
746
|
+
const category = normalizePluginCategory(entry.category);
|
|
747
|
+
if (category === "app" || plugins.has(pluginId)) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const active = isPluginLoaded(pluginId, entry.npmName, loadedNames);
|
|
751
|
+
const persistedEnabled = resolvePersistedPluginEnabled(
|
|
752
|
+
pluginId,
|
|
753
|
+
category,
|
|
754
|
+
entry.npmName,
|
|
755
|
+
configEntries,
|
|
756
|
+
configRecord
|
|
757
|
+
);
|
|
758
|
+
plugins.set(pluginId, {
|
|
759
|
+
id: pluginId,
|
|
760
|
+
name: entry.name,
|
|
761
|
+
description: entry.description,
|
|
762
|
+
tags: entry.tags,
|
|
763
|
+
enabled: resolveCompatPluginEnabledForList(active, persistedEnabled),
|
|
764
|
+
configured: entry.configured,
|
|
765
|
+
envKey: entry.envKey,
|
|
766
|
+
category,
|
|
767
|
+
source: entry.source,
|
|
768
|
+
configKeys: entry.configKeys,
|
|
769
|
+
parameters: entry.parameters,
|
|
770
|
+
validationErrors: entry.validationErrors,
|
|
771
|
+
validationWarnings: entry.validationWarnings,
|
|
772
|
+
npmName: entry.npmName,
|
|
773
|
+
version: resolveInstalledPackageVersion(entry.npmName) ?? entry.version ?? void 0,
|
|
774
|
+
pluginDeps: entry.pluginDeps,
|
|
775
|
+
isActive: active,
|
|
776
|
+
configUiHints: entry.configUiHints,
|
|
777
|
+
icon: entry.icon ?? null,
|
|
778
|
+
homepage: entry.homepage,
|
|
779
|
+
repository: entry.repository,
|
|
780
|
+
setupGuideUrl: entry.setupGuideUrl,
|
|
781
|
+
visible: true
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
for (const plugin of runtime?.plugins ?? []) {
|
|
785
|
+
const pluginName = typeof plugin.name === "string" ? plugin.name : "";
|
|
786
|
+
if (!pluginName) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const pluginId = normalizePluginId(pluginName);
|
|
790
|
+
const existing = plugins.get(pluginId);
|
|
791
|
+
if (existing) {
|
|
792
|
+
existing.isActive = true;
|
|
793
|
+
if (existing.enabled !== true && configEntries[pluginId]?.enabled == null) {
|
|
794
|
+
existing.enabled = true;
|
|
795
|
+
}
|
|
796
|
+
if (!existing.version) {
|
|
797
|
+
existing.version = resolveInstalledPackageVersion(pluginName) ?? void 0;
|
|
798
|
+
}
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
plugins.set(pluginId, {
|
|
802
|
+
id: pluginId,
|
|
803
|
+
name: titleCasePluginId(pluginId),
|
|
804
|
+
description: plugin.description ?? "Loaded runtime plugin discovered without manifest metadata.",
|
|
805
|
+
tags: [],
|
|
806
|
+
enabled: typeof configEntries[pluginId]?.enabled === "boolean" ? Boolean(configEntries[pluginId]?.enabled) : true,
|
|
807
|
+
configured: true,
|
|
808
|
+
envKey: null,
|
|
809
|
+
category: "feature",
|
|
810
|
+
source: "bundled",
|
|
811
|
+
parameters: [],
|
|
812
|
+
validationErrors: [],
|
|
813
|
+
validationWarnings: [],
|
|
814
|
+
npmName: pluginName,
|
|
815
|
+
version: resolveInstalledPackageVersion(pluginName) ?? void 0,
|
|
816
|
+
isActive: true,
|
|
817
|
+
icon: null
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
for (const [pluginName, installRecord] of Object.entries(installEntries)) {
|
|
821
|
+
const pluginId = normalizePluginId(pluginName);
|
|
822
|
+
if (plugins.has(pluginId)) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
plugins.set(pluginId, {
|
|
826
|
+
id: pluginId,
|
|
827
|
+
name: titleCasePluginId(pluginId),
|
|
828
|
+
description: "Installed store plugin.",
|
|
829
|
+
tags: [],
|
|
830
|
+
enabled: typeof configEntries[pluginId]?.enabled === "boolean" ? Boolean(configEntries[pluginId]?.enabled) : false,
|
|
831
|
+
configured: true,
|
|
832
|
+
envKey: null,
|
|
833
|
+
category: "feature",
|
|
834
|
+
source: "store",
|
|
835
|
+
parameters: [],
|
|
836
|
+
validationErrors: [],
|
|
837
|
+
validationWarnings: [],
|
|
838
|
+
npmName: pluginName,
|
|
839
|
+
version: typeof installRecord.version === "string" ? installRecord.version : resolveInstalledPackageVersion(pluginName) ?? void 0,
|
|
840
|
+
isActive: isPluginLoaded(pluginId, pluginName, loadedNames),
|
|
841
|
+
icon: null
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
const pluginList = Array.from(plugins.values()).sort(
|
|
845
|
+
(left, right) => String(left.name ?? "").localeCompare(String(right.name ?? ""))
|
|
846
|
+
);
|
|
847
|
+
return { plugins: pluginList };
|
|
848
|
+
}
|
|
849
|
+
function validateCompatPluginConfig(plugin, config) {
|
|
850
|
+
const paramMap = new Map(
|
|
851
|
+
plugin.parameters.map((parameter) => [parameter.key, parameter])
|
|
852
|
+
);
|
|
853
|
+
const errors = [];
|
|
854
|
+
const values = {};
|
|
855
|
+
for (const [key, rawValue] of Object.entries(config)) {
|
|
856
|
+
const parameter = paramMap.get(key);
|
|
857
|
+
if (!parameter) {
|
|
858
|
+
errors.push({
|
|
859
|
+
field: key,
|
|
860
|
+
message: `${key} is not a declared config key for this plugin`
|
|
861
|
+
});
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (typeof rawValue !== "string") {
|
|
865
|
+
errors.push({
|
|
866
|
+
field: key,
|
|
867
|
+
message: "Plugin config values must be strings."
|
|
868
|
+
});
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const trimmed = rawValue.trim();
|
|
872
|
+
if (parameter.required && trimmed.length === 0) {
|
|
873
|
+
errors.push({
|
|
874
|
+
field: key,
|
|
875
|
+
message: "Required value is not configured."
|
|
876
|
+
});
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
values[key] = rawValue;
|
|
880
|
+
}
|
|
881
|
+
return { errors, values };
|
|
882
|
+
}
|
|
883
|
+
function persistCompatPluginMutation(pluginId, body, plugin) {
|
|
884
|
+
const config = loadElizaConfig();
|
|
885
|
+
const configRecord = config;
|
|
886
|
+
config.plugins ??= {};
|
|
887
|
+
config.plugins.entries ??= {};
|
|
888
|
+
config.plugins.entries[pluginId] ??= {};
|
|
889
|
+
const pluginEntry = config.plugins.entries[pluginId];
|
|
890
|
+
if (typeof body.enabled === "boolean") {
|
|
891
|
+
pluginEntry.enabled = body.enabled;
|
|
892
|
+
if (CAPABILITY_FEATURE_IDS.has(pluginId)) {
|
|
893
|
+
config.features ??= {};
|
|
894
|
+
config.features[pluginId] = body.enabled;
|
|
895
|
+
}
|
|
896
|
+
if (plugin.category === "connector") {
|
|
897
|
+
writeCompatSectionEnabled(
|
|
898
|
+
configRecord,
|
|
899
|
+
"connectors",
|
|
900
|
+
resolveCompatConfigKey(pluginId, plugin.npmName, CONNECTOR_PLUGINS),
|
|
901
|
+
body.enabled
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
if (plugin.category === "streaming") {
|
|
905
|
+
writeCompatSectionEnabled(
|
|
906
|
+
configRecord,
|
|
907
|
+
"streaming",
|
|
908
|
+
resolveCompatConfigKey(pluginId, plugin.npmName, STREAMING_PLUGINS),
|
|
909
|
+
body.enabled
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (body.config !== void 0) {
|
|
914
|
+
if (!body.config || typeof body.config !== "object" || Array.isArray(body.config)) {
|
|
915
|
+
return {
|
|
916
|
+
status: 400,
|
|
917
|
+
payload: { ok: false, error: "Plugin config must be a JSON object." }
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const configObject = body.config;
|
|
921
|
+
const { errors, values } = validateCompatPluginConfig(plugin, configObject);
|
|
922
|
+
if (errors.length > 0) {
|
|
923
|
+
return {
|
|
924
|
+
status: 422,
|
|
925
|
+
payload: { ok: false, plugin, validationErrors: errors }
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
const nextConfig = pluginEntry.config && typeof pluginEntry.config === "object" && !Array.isArray(pluginEntry.config) ? { ...pluginEntry.config } : {};
|
|
929
|
+
config.env ??= {};
|
|
930
|
+
for (const [key, value] of Object.entries(values)) {
|
|
931
|
+
if (value.trim()) {
|
|
932
|
+
config.env[key] = value;
|
|
933
|
+
nextConfig[key] = value;
|
|
934
|
+
} else {
|
|
935
|
+
delete config.env[key];
|
|
936
|
+
delete nextConfig[key];
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
pluginEntry.config = nextConfig;
|
|
940
|
+
if (plugin.category === "connector") {
|
|
941
|
+
syncCompatConnectorConfigValues(
|
|
942
|
+
configRecord,
|
|
943
|
+
pluginId,
|
|
944
|
+
plugin.npmName,
|
|
945
|
+
values
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
saveElizaConfig(config);
|
|
949
|
+
for (const [key, value] of Object.entries(values)) {
|
|
950
|
+
try {
|
|
951
|
+
if (value.trim()) {
|
|
952
|
+
process.env[key] = value;
|
|
953
|
+
} else {
|
|
954
|
+
delete process.env[key];
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
saveElizaConfig(config);
|
|
961
|
+
}
|
|
962
|
+
const refreshed = buildPluginListResponse(null).plugins.find(
|
|
963
|
+
(candidate) => candidate.id === pluginId
|
|
964
|
+
);
|
|
965
|
+
return {
|
|
966
|
+
status: 200,
|
|
967
|
+
payload: {
|
|
968
|
+
ok: true,
|
|
969
|
+
plugin: refreshed ?? plugin
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
async function handlePluginsCompatRoutes(req, res, state) {
|
|
974
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
975
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
976
|
+
if (!url.pathname.startsWith("/api/plugins")) {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
if (method === "GET" && url.pathname === "/api/plugins") {
|
|
980
|
+
if (!await ensureRouteAuthorized(req, res, state)) {
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
const pluginResponse = buildPluginListResponse(state.current);
|
|
984
|
+
logger.debug(
|
|
985
|
+
`[api/plugins] source=registry total=${pluginResponse.plugins.length} runtime=${state.current ? "active" : "null"}`
|
|
986
|
+
);
|
|
987
|
+
maybeLogPluginStateDrift(buildPluginDriftDiagnostics(state.current));
|
|
988
|
+
sendJsonResponse(res, 200, pluginResponse);
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
if (method === "GET" && url.pathname === "/api/plugins/diagnostics") {
|
|
992
|
+
if (!await ensureRouteAuthorized(req, res, state)) {
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
const diagnostics = buildPluginDriftDiagnostics(state.current);
|
|
996
|
+
maybeLogPluginStateDrift(diagnostics);
|
|
997
|
+
sendJsonResponse(res, 200, diagnostics);
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
if (method === "PUT" && url.pathname.startsWith("/api/plugins/")) {
|
|
1001
|
+
if (!await ensureRouteAuthorized(req, res, state)) {
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
const body = await readCompatJsonBody(req, res);
|
|
1005
|
+
if (body == null) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
const decodedPluginId = decodePluginPathSegment(
|
|
1009
|
+
url.pathname.slice("/api/plugins/".length)
|
|
1010
|
+
);
|
|
1011
|
+
if (decodedPluginId === null) {
|
|
1012
|
+
sendJsonErrorResponse(res, 400, "Invalid plugin path");
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
const pluginId = normalizePluginId(decodedPluginId);
|
|
1016
|
+
const plugin = buildPluginListResponse(state.current).plugins.find(
|
|
1017
|
+
(candidate) => candidate.id === pluginId
|
|
1018
|
+
);
|
|
1019
|
+
if (!plugin) {
|
|
1020
|
+
sendJsonErrorResponse(res, 404, `Plugin "${pluginId}" not found`);
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
const previousConfig = structuredClone(loadElizaConfig());
|
|
1024
|
+
const result = persistCompatPluginMutation(pluginId, body, plugin);
|
|
1025
|
+
if (result.status === 200) {
|
|
1026
|
+
const nextConfig = loadElizaConfig();
|
|
1027
|
+
const runtimeApply = await applyCompatRuntimeMutation({
|
|
1028
|
+
state,
|
|
1029
|
+
pluginId,
|
|
1030
|
+
plugin,
|
|
1031
|
+
body,
|
|
1032
|
+
previousConfig,
|
|
1033
|
+
nextConfig
|
|
1034
|
+
});
|
|
1035
|
+
if (runtimeApply.requiresRestart) {
|
|
1036
|
+
scheduleCompatRuntimeRestart(state, runtimeApply.reason);
|
|
1037
|
+
}
|
|
1038
|
+
const refreshed = buildPluginListResponse(state.current).plugins.find(
|
|
1039
|
+
(candidate) => candidate.id === pluginId
|
|
1040
|
+
);
|
|
1041
|
+
result.payload.plugin = refreshed ?? result.payload.plugin ?? plugin;
|
|
1042
|
+
result.payload.applied = runtimeApply.mode;
|
|
1043
|
+
result.payload.requiresRestart = runtimeApply.requiresRestart;
|
|
1044
|
+
result.payload.restartedRuntime = runtimeApply.restartedRuntime;
|
|
1045
|
+
result.payload.loadedPackages = runtimeApply.loadedPackages;
|
|
1046
|
+
result.payload.unloadedPackages = runtimeApply.unloadedPackages;
|
|
1047
|
+
result.payload.reloadedPackages = runtimeApply.reloadedPackages;
|
|
1048
|
+
const mirrorResult = await mirrorPluginSensitiveToVault(plugin, body);
|
|
1049
|
+
if (mirrorResult.failures.length > 0) {
|
|
1050
|
+
result.payload.vaultMirrorFailures = mirrorResult.failures;
|
|
1051
|
+
}
|
|
1052
|
+
const diagnostics = buildPluginDriftDiagnostics(state.current);
|
|
1053
|
+
if (diagnostics.summary.withDrift > 0) {
|
|
1054
|
+
result.payload.diagnostics = diagnostics;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
sendJsonResponse(res, result.status, result.payload);
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
const testMatch = method === "POST" && url.pathname.match(/^\/api\/plugins\/([^/]+)\/test$/);
|
|
1061
|
+
if (testMatch) {
|
|
1062
|
+
if (!await ensureRouteAuthorized(req, res, state)) return true;
|
|
1063
|
+
const decodedTestPluginId = decodePluginPathSegment(testMatch[1]);
|
|
1064
|
+
if (decodedTestPluginId === null) {
|
|
1065
|
+
sendJsonErrorResponse(res, 400, "Invalid plugin path");
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
const testPluginId = normalizePluginId(decodedTestPluginId);
|
|
1069
|
+
const startMs = Date.now();
|
|
1070
|
+
if (testPluginId === "telegram") {
|
|
1071
|
+
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
1072
|
+
if (!token) {
|
|
1073
|
+
sendJsonResponse(res, 422, {
|
|
1074
|
+
success: false,
|
|
1075
|
+
pluginId: testPluginId,
|
|
1076
|
+
error: "No bot token configured",
|
|
1077
|
+
durationMs: Date.now() - startMs
|
|
1078
|
+
});
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
const apiRoot = process.env.TELEGRAM_API_ROOT || "https://api.telegram.org";
|
|
1083
|
+
const tgResp = await fetch(`${apiRoot}/bot${token}/getMe`);
|
|
1084
|
+
const tgData = await tgResp.json();
|
|
1085
|
+
sendJsonResponse(res, tgData.ok ? 200 : 422, {
|
|
1086
|
+
success: tgData.ok,
|
|
1087
|
+
pluginId: testPluginId,
|
|
1088
|
+
message: tgData.ok ? `Connected as @${tgData.result?.username}` : `Telegram API error: ${tgData.description}`,
|
|
1089
|
+
durationMs: Date.now() - startMs
|
|
1090
|
+
});
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
sendJsonResponse(res, 422, {
|
|
1093
|
+
success: false,
|
|
1094
|
+
pluginId: testPluginId,
|
|
1095
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1096
|
+
durationMs: Date.now() - startMs
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
sendJsonResponse(res, 200, {
|
|
1102
|
+
success: true,
|
|
1103
|
+
pluginId: testPluginId,
|
|
1104
|
+
message: "Plugin is loaded (no custom test available)",
|
|
1105
|
+
durationMs: Date.now() - startMs
|
|
1106
|
+
});
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
const revealMatch = method === "POST" && url.pathname.match(/^\/api\/plugins\/([^/]+)\/reveal$/);
|
|
1110
|
+
if (revealMatch) {
|
|
1111
|
+
if (!await ensureRouteAuthorized(req, res, state)) return true;
|
|
1112
|
+
const revealBody = await readCompatJsonBody(req, res);
|
|
1113
|
+
if (revealBody == null) return true;
|
|
1114
|
+
const key = revealBody.key?.trim();
|
|
1115
|
+
if (!key) {
|
|
1116
|
+
sendJsonErrorResponse(res, 400, "Missing key parameter");
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
const upperKey = key.toUpperCase();
|
|
1120
|
+
if (!REVEALABLE_KEY_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
|
|
1121
|
+
sendJsonErrorResponse(
|
|
1122
|
+
res,
|
|
1123
|
+
403,
|
|
1124
|
+
"Key is not in the allowlist of revealable plugin config keys"
|
|
1125
|
+
);
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
if (SENSITIVE_KEY_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
|
|
1129
|
+
if (!ensureCompatSensitiveRouteAuthorized(req, res)) return true;
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
const decodedRevealPluginId = decodePluginPathSegment(revealMatch[1]);
|
|
1133
|
+
if (decodedRevealPluginId === null) {
|
|
1134
|
+
sendJsonErrorResponse(res, 400, "Invalid plugin path");
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
const vaultValue = await sharedVault().reveal(
|
|
1138
|
+
key,
|
|
1139
|
+
`plugins:${decodedRevealPluginId}:reveal`
|
|
1140
|
+
);
|
|
1141
|
+
sendJsonResponse(res, 200, { ok: true, value: vaultValue });
|
|
1142
|
+
return true;
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (!(err instanceof VaultMissError)) {
|
|
1145
|
+
logger.warn(
|
|
1146
|
+
`[api/plugins] Vault reveal failed for ${key}: ${err instanceof Error ? err.message : String(err)}`
|
|
1147
|
+
);
|
|
1148
|
+
sendJsonErrorResponse(res, 500, "Vault reveal failed");
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const config = loadElizaConfig();
|
|
1153
|
+
const fallbackValue = process.env[key] ?? config.env?.[key] ?? null;
|
|
1154
|
+
if (typeof fallbackValue === "string" && isVaultRef(fallbackValue)) {
|
|
1155
|
+
const innerKey = parseVaultRef(fallbackValue);
|
|
1156
|
+
if (innerKey) {
|
|
1157
|
+
try {
|
|
1158
|
+
const inner = await sharedVault().get(innerKey);
|
|
1159
|
+
if (inner) {
|
|
1160
|
+
sendJsonResponse(res, 200, { ok: true, value: inner });
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
sendJsonResponse(res, 200, { ok: true, value: null });
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
sendJsonResponse(res, 200, { ok: true, value: fallbackValue });
|
|
1170
|
+
return true;
|
|
1171
|
+
}
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
export {
|
|
1175
|
+
_resetSharedVaultForTesting,
|
|
1176
|
+
analyzePluginStateDrift,
|
|
1177
|
+
buildPluginListResponse,
|
|
1178
|
+
handlePluginsCompatRoutes,
|
|
1179
|
+
mirrorPluginSensitiveToVault,
|
|
1180
|
+
persistCompatPluginMutation,
|
|
1181
|
+
resolveAdvancedCapabilityCompatStatus,
|
|
1182
|
+
resolveCompatPluginEnabledForList,
|
|
1183
|
+
resolvePluginManifestPath
|
|
1184
|
+
};
|
|
1185
|
+
//# sourceMappingURL=app-plugins-routes.js.map
|