diff --git a/agent/src/index.ts b/agent/src/index.ts index 8ee5d362b..35b23b8c3 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -25,39 +25,13 @@ import { stringToUuid, validateCharacterConfig, CacheStore, + getSecret, + loadPlugins, } from "@elizaos/core"; import { RedisClient } from "@elizaos/adapter-redis"; -import { zgPlugin } from "@elizaos/plugin-0g"; import { bootstrapPlugin } from "@elizaos/plugin-bootstrap"; -import createGoatPlugin from "@elizaos/plugin-goat"; -// import { intifacePlugin } from "@elizaos/plugin-intiface"; import { DirectClient } from "@elizaos/client-direct"; -import { aptosPlugin } from "@elizaos/plugin-aptos"; -import { - advancedTradePlugin, - coinbaseCommercePlugin, - coinbaseMassPaymentsPlugin, - tokenContractPlugin, - tradePlugin, - webhookPlugin, -} from "@elizaos/plugin-coinbase"; -import { confluxPlugin } from "@elizaos/plugin-conflux"; -import { evmPlugin } from "@elizaos/plugin-evm"; -import { storyPlugin } from "@elizaos/plugin-story"; -import { flowPlugin } from "@elizaos/plugin-flow"; -import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; -import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation"; -import { multiversxPlugin } from "@elizaos/plugin-multiversx"; -import { nearPlugin } from "@elizaos/plugin-near"; -import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; import { createNodePlugin } from "@elizaos/plugin-node"; -import { solanaPlugin } from "@elizaos/plugin-solana"; -import { suiPlugin } from "@elizaos/plugin-sui"; -import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; -import { tonPlugin } from "@elizaos/plugin-ton"; -import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; -import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; -import { abstractPlugin } from "@elizaos/plugin-abstract"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -451,10 +425,6 @@ export async function initializeClients( return clients; } -function getSecret(character: Character, secret: string) { - return character.settings?.secrets?.[secret] || process.env[secret]; -} - let nodePlugin: any | undefined; export async function createAgent( @@ -475,19 +445,155 @@ export async function createAgent( const walletSecretSalt = getSecret(character, "WALLET_SECRET_SALT"); // Validate TEE configuration - if (teeMode !== TEEMode.OFF && !walletSecretSalt) { + if (teeMode !== "OFF" && !walletSecretSalt) { elizaLogger.error( "WALLET_SECRET_SALT required when TEE_MODE is enabled" ); throw new Error("Invalid TEE configuration"); } - let goatPlugin: any | undefined; - if (getSecret(character, "EVM_PROVIDER_URL")) { - goatPlugin = await createGoatPlugin((secret) => - getSecret(character, secret) - ); - } + const dynamicPlugins = await loadPlugins(character, [ + { + secrets: ["COINBASE_API_KEY", "COINBASE_PRIVATE_KEY"], + importFn: () => + import("@elizaos/plugin-coinbase").then((m) => [ + m.coinbaseMassPaymentsPlugin, + m.tradePlugin, + m.tokenContractPlugin, + m.advancedTradePlugin, + ]), + }, + { + secrets: "COINBASE_COMMERCE_KEY", + importFn: () => + import("@elizaos/plugin-coinbase").then( + (m) => m.coinbaseCommercePlugin + ), + }, + { + secrets: [ + "COINBASE_API_KEY", + "COINBASE_PRIVATE_KEY", + "COINBASE_NOTIFICATION_URI", + ], + importFn: () => + import("@elizaos/plugin-coinbase").then((m) => m.webhookPlugin), + }, + { + secrets: "CONFLUX_CORE_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-conflux").then((m) => m.confluxPlugin), + }, + { + secrets: "SOLANA_PUBLIC_KEY", + importFn: () => + import("@elizaos/plugin-solana").then((m) => m.solanaPlugin), + }, + { + secrets: [ + "NEAR_ADDRESS", + "NEAR_WALLET_PUBLIC_KEY", + "NEAR_WALLET_SECRET_KEY", + ], + importFn: () => + import("@elizaos/plugin-near").then((m) => m.nearPlugin), + }, + { + secrets: "EVM_PUBLIC_KEY", + importFn: () => + import("@ai16z/plugin-evm").then((m) => m.evmPlugin), + }, + { + secrets: [ + "SOLANA_PUBLIC_KEY", + "SOLANA_ADMIN_PUBLIC_KEY", + "SOLANA_PRIVATE_KEY", + "SOLANA_ADMIN_PRIVATE_KEY", + ], + importFn: () => + import("@elizaos/plugin-nft-generation").then( + (m) => m.nftGenerationPlugin + ), + }, + { + secrets: "ZEROG_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-0g").then((m) => m.zgPlugin), + }, + { + secrets: [ + "FAL_API_KEY", + "OPENAI_API_KEY", + "VENICE_API_KEY", + "HEURIST_API_KEY", + ], + importFn: () => + import("@elizaos/plugin-image-generation").then( + (m) => m.imageGenerationPlugin + ), + }, + { + secrets: ["ABSTRACT_PRIVATE_KEY"], + importFn: () => + import("@elizaos/plugin-abstract").then( + (m) => m.abstractPlugin + ), + }, + { + secrets: ["FLOW_ADDRESS", "FLOW_PRIVATE_KEY"], + importFn: () => + import("@elizaos/plugin-flow").then((m) => m.flowPlugin), + }, + { + secrets: "APTOS_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-aptos").then((m) => m.aptosPlugin), + }, + { + secrets: "MVX_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-multiversx").then( + (m) => m.multiversxPlugin + ), + }, + { + secrets: "ZKSYNC_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-zksync-era").then( + (m) => m.zksyncEraPlugin + ), + }, + { + secrets: "TON_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-ton").then((m) => m.tonPlugin), + }, + { + secrets: "SUI_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-sui").then((m) => m.suiPlugin), + }, + { + secrets: "STORY_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-story").then((m) => m.storyPlugin), + }, + { + secrets: "CRONOSZKEVM_PRIVATE_KEY", + importFn: () => + import("@elizaos/plugin-cronoszkevm").then( + (m) => m.cronosZkEVMPlugin + ), + }, + ...(teeMode === "OFF" && + walletSecretSalt && [ + { + secrets: [], + importFn: () => + import("@elizaos/plugin-tee").then((m) => m.teePlugin), + }, + ]), + ]); return new AgentRuntime({ databaseAdapter: db, @@ -496,83 +602,9 @@ export async function createAgent( evaluators: [], character, // character.plugins are handled when clients are added - plugins: [ - bootstrapPlugin, - getSecret(character, "CONFLUX_CORE_PRIVATE_KEY") - ? confluxPlugin - : null, - nodePlugin, - getSecret(character, "SOLANA_PUBLIC_KEY") || - (getSecret(character, "WALLET_PUBLIC_KEY") && - !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) - ? solanaPlugin - : null, - (getSecret(character, "NEAR_ADDRESS") || - getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) && - getSecret(character, "NEAR_WALLET_SECRET_KEY") - ? nearPlugin - : null, - getSecret(character, "EVM_PUBLIC_KEY") || - (getSecret(character, "WALLET_PUBLIC_KEY") && - getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) - ? evmPlugin - : null, - (getSecret(character, "SOLANA_PUBLIC_KEY") || - (getSecret(character, "WALLET_PUBLIC_KEY") && - !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith( - "0x" - ))) && - getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") && - getSecret(character, "SOLANA_PRIVATE_KEY") && - getSecret(character, "SOLANA_ADMIN_PRIVATE_KEY") - ? nftGenerationPlugin - : null, - getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, - getSecret(character, "COINBASE_COMMERCE_KEY") - ? coinbaseCommercePlugin - : null, - getSecret(character, "FAL_API_KEY") || - getSecret(character, "OPENAI_API_KEY") || - getSecret(character, "VENICE_API_KEY") || - getSecret(character, "HEURIST_API_KEY") - ? imageGenerationPlugin - : null, - getSecret(character, "FAL_API_KEY") ? ThreeDGenerationPlugin : null, - ...(getSecret(character, "COINBASE_API_KEY") && - getSecret(character, "COINBASE_PRIVATE_KEY") - ? [ - coinbaseMassPaymentsPlugin, - tradePlugin, - tokenContractPlugin, - advancedTradePlugin, - ] - : []), - ...(teeMode !== TEEMode.OFF && walletSecretSalt - ? [teePlugin, solanaPlugin] - : []), - getSecret(character, "COINBASE_API_KEY") && - getSecret(character, "COINBASE_PRIVATE_KEY") && - getSecret(character, "COINBASE_NOTIFICATION_URI") - ? webhookPlugin - : null, - getSecret(character, "EVM_PROVIDER_URL") ? goatPlugin : null, - getSecret(character, "ABSTRACT_PRIVATE_KEY") - ? abstractPlugin - : null, - getSecret(character, "FLOW_ADDRESS") && - getSecret(character, "FLOW_PRIVATE_KEY") - ? flowPlugin - : null, - getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, - getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, - getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, - getSecret(character, "CRONOSZKEVM_PRIVATE_KEY") - ? cronosZkEVMPlugin - : null, - getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, - getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, - getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, - ].filter(Boolean), + plugins: [bootstrapPlugin, nodePlugin, ...dynamicPlugins].filter( + Boolean + ), providers: [], actions: [], services: [], diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c0360768e..d67d5c35b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,5 +22,6 @@ export * from "./parsing.ts"; export * from "./uuid.ts"; export * from "./environment.ts"; export * from "./cache.ts"; +export * from "./plugins.ts"; export { default as knowledge } from "./knowledge.ts"; export * from "./utils.ts"; diff --git a/packages/core/src/plugins.ts b/packages/core/src/plugins.ts new file mode 100644 index 000000000..37a524be1 --- /dev/null +++ b/packages/core/src/plugins.ts @@ -0,0 +1,52 @@ +import { Character, DynamicPlugin, Plugin } from "./types"; + +export function getSecret( + character: Character, + secret: string +): string | undefined { + return character.settings?.secrets?.[secret] ?? process.env[secret]; +} + +export async function loadPlugin( + keys: string | string[], + character: Character, + importFn: () => Promise +): Promise { + const keyArray = Array.isArray(keys) ? keys : [keys]; + const hasAllSecrets = + keyArray.every((key) => getSecret(character, key)) || + keyArray.length === 0; + if (!hasAllSecrets) return null; + return importFn(); +} + +const importCache = new Map< + () => Promise, + Promise +>(); + +export async function loadPlugins( + character: Character, + definitions: DynamicPlugin[] +): Promise { + const plugins: Plugin[] = []; + + for (const def of definitions) { + const keyArray = Array.isArray(def.secrets) + ? def.secrets + : [def.secrets]; + const hasAllSecrets = + keyArray.every((key) => getSecret(character, key)) || + keyArray.length === 0; + if (!hasAllSecrets) continue; + + let importPromise = importCache.get(def.importFn); + if (!importPromise) { + importPromise = def.importFn(); + importCache.set(def.importFn, importPromise); + } + const result = await importPromise; + plugins.push(...(Array.isArray(result) ? result : [result])); + } + return plugins; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 6786bb99f..6294dc168 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -608,6 +608,11 @@ export type Plugin = { clients?: Client[]; }; +export interface DynamicPlugin { + secrets: string | string[]; + importFn: () => Promise; +} + /** * Available client platforms */ diff --git a/scripts/dev.sh b/scripts/dev.sh index 22b5466f9..ed55e1362 100644 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -42,19 +42,17 @@ cat << "EOF" * * * "@elizaos/your-plugin-name": "workspace:*" * * * -* 5. Edit the 'index.ts' file in 'agent/src': * -* * -* a. Import your plugin: * -* * -* import yourPlugin from '@elizaos/your-plugin-name'; * -* * -* b. Add your plugin to the `plugins` array: * -* * -* const plugins = [ * -* existingPlugin, * -* yourPlugin, * -* ]; * -* * +* 5. Edit the 'index.ts' file in 'agent/src': * +* * +* a. Use the `loadPlugins` function to dynamically import your plugin: * +* * +* { * +* secrets: ["YOUR_PLUGIN_SECRET_KEY"], * +* importFn: () => import("path/to/your-plugin").then((m) => m.yourPluginExport), * +* }, * +* * +* b. Add this object to the array passed to the `loadPlugins` function. * +* * * This will ensure that your plugin's development server runs * * alongside others when you execute this script. * ***********************************************************************