diff --git a/.env.example b/.env.example index 9a56c9df13e..fb44e9ff57c 100644 --- a/.env.example +++ b/.env.example @@ -505,7 +505,9 @@ GIPHY_API_KEY= # OpenWeather OPEN_WEATHER_API_KEY= # OpenWeather API key - +#GITCOIN Passport +PASSPORT_API_KEY= #Gitcoin Passport key +PASSPORT_SCORER= #Scorer number # EchoChambers Configuration ECHOCHAMBERS_API_URL=http://127.0.0.1:3333 diff --git a/agent/package.json b/agent/package.json index 1e6e261939e..73cd235caa9 100644 --- a/agent/package.json +++ b/agent/package.json @@ -50,6 +50,7 @@ "@elizaos/plugin-flow": "workspace:*", "@elizaos/plugin-gitbook": "workspace:*", "@elizaos/plugin-story": "workspace:*", + "@elizaos/plugin-gitcoin-passport": "workspace:*", "@elizaos/plugin-goat": "workspace:*", "@elizaos/plugin-lensNetwork": "workspace:*", "@elizaos/plugin-icp": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 1b2846a92a3..df62d2ef020 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -71,6 +71,7 @@ import { flowPlugin } from "@elizaos/plugin-flow"; import { fuelPlugin } from "@elizaos/plugin-fuel"; import { genLayerPlugin } from "@elizaos/plugin-genlayer"; import { giphyPlugin } from "@elizaos/plugin-giphy"; +import { gitcoinPassportPlugin } from "@elizaos/plugin-gitcoin-passport"; import { hyperliquidPlugin } from "@elizaos/plugin-hyperliquid"; import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; import { lensPlugin } from "@elizaos/plugin-lensNetwork"; @@ -826,7 +827,7 @@ export async function createAgent( getSecret(character, "ABSTRACT_PRIVATE_KEY") ? abstractPlugin : null, - getSecret(character, "B2_PRIVATE_KEY") ? b2Plugin: null, + getSecret(character, "B2_PRIVATE_KEY") ? b2Plugin : null, getSecret(character, "BINANCE_API_KEY") && getSecret(character, "BINANCE_SECRET_KEY") ? binancePlugin @@ -869,6 +870,9 @@ export async function createAgent( getSecret(character, "LETZAI_API_KEY") ? letzAIPlugin : null, getSecret(character, "STARGAZE_ENDPOINT") ? stargazePlugin : null, getSecret(character, "GIPHY_API_KEY") ? giphyPlugin : null, + getSecret(character, "PASSPORT_API_KEY") + ? gitcoinPassportPlugin + : null, getSecret(character, "GENLAYER_PRIVATE_KEY") ? genLayerPlugin : null, diff --git a/client/src/components/chat.tsx b/client/src/components/chat.tsx index b54521de539..8173e99da55 100644 --- a/client/src/components/chat.tsx +++ b/client/src/components/chat.tsx @@ -61,10 +61,13 @@ export default function Page({ agentId }: { agentId: UUID }) { const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (e.nativeEvent.isComposing) return; handleSendMessage(e as unknown as React.FormEvent); } }; + const handleSendMessage = (e: React.FormEvent) => { e.preventDefault(); if (!input) return; diff --git a/docs/docs/advanced/eliza-in-tee.md b/docs/docs/advanced/eliza-in-tee.md index 76d2e9e2851..56eaf9012b3 100644 --- a/docs/docs/advanced/eliza-in-tee.md +++ b/docs/docs/advanced/eliza-in-tee.md @@ -57,16 +57,22 @@ Example usage: const provider = new DeriveKeyProvider(teeMode); // For Solana const { keypair, attestation } = await provider.deriveEd25519Keypair( - "/", secretSalt, + "solana", agentId, ); // For EVM const { keypair, attestation } = await provider.deriveEcdsaKeypair( - "/", secretSalt, + "evm", agentId, ); + +// For raw key derivation +const rawKey = await provider.deriveRawKey( + secretSalt, + "raw", +); ``` --- @@ -161,7 +167,7 @@ To set up your environment for TEE development: 4. **Verify TEE Attestation** - You can verify the TEE attestation quote by going to the [TEE RA Explorer](https://ra-quote-explorer.vercel.app/) and pasting the attestation quote from the agent logs. Here's an example of interacting with the Eliza agent to ask for the agent's wallet address: + You can verify the TEE attestation quote by going to the [TEE RA Explorer](https://proof.t16z.com/) and pasting the attestation quote from the agent logs. Here's an example of interacting with the Eliza agent to ask for the agent's wallet address: ```bash You: what's your wallet address? @@ -227,7 +233,7 @@ Now we are ready to deploy the Eliza agent to a real TEE environment. ### Run an Eliza Agent in a Real TEE Environment -Before deploying the Eliza agent to a real TEE environment, you need to create a new TEE account on the [TEE Cloud](https://teehouse.vercel.app). Reach out to Phala Network on [Discord](https://discord.gg/phalanetwork) if you need help. +Before deploying the Eliza agent to a real TEE environment, you need to create a new TEE account on the [TEE Cloud](https://cloud.phala.network/login). Reach out to Phala Network on [Discord](https://discord.gg/phalanetwork) if you need help. Next, you will need to take the docker-compose.yaml file in the root folder of the project and edit it based on your agent configuration. @@ -285,7 +291,7 @@ volumes: tee: ``` -Now you can deploy the Eliza agent to a real TEE environment. Go to the [TEE Cloud](https://teehouse.vercel.app) and click on the `Create VM` button to configure your Eliza agent deployment. +Now you can deploy the Eliza agent to a real TEE environment. Go to the [TEE Cloud](https://cloud.phala.network/login) and click on the `Create VM` button to configure your Eliza agent deployment. Click on the `Compose Manifest Mode` tab and paste the docker-compose.yaml file content into the `Compose Manifest` field. @@ -313,7 +319,7 @@ Click on the `Logs` tab to view the agent logs. ![Agent Logs](https://i.imgur.com/aU3i0Dv.png) -Now we can verify the REAL TEE attestation quote by going to the [TEE RA Explorer](https://ra-quote-explorer.vercel.app/) and pasting the attestation quote from the agent logs. +Now we can verify the REAL TEE attestation quote by going to the [TEE RA Explorer](https://proof.t16z.com/) and pasting the attestation quote from the agent logs. ![TEE RA Explorer](https://i.imgur.com/TJ5299l.png) diff --git a/docs/docs/packages/plugins.md b/docs/docs/packages/plugins.md index c1142c79ecc..18bafb92fe0 100644 --- a/docs/docs/packages/plugins.md +++ b/docs/docs/packages/plugins.md @@ -469,7 +469,7 @@ const provider = new DeriveKeyProvider(); // Derive a raw key try { const rawKey = await provider.rawDeriveKey( - "/path/to/derive", + "/path/to/derive", // This is what the WALLET_SECRET_SALT is used for "subject-identifier", ); // rawKey is a DeriveKeyResponse that can be used for further processing @@ -482,7 +482,7 @@ try { // Derive a Solana keypair (Ed25519) try { const solanaKeypair = await provider.deriveEd25519Keypair( - "/path/to/derive", + "/path/to/derive", // This is what the WALLET_SECRET_SALT is used for "subject-identifier", ); // solanaKeypair can now be used for Solana operations @@ -493,7 +493,7 @@ try { // Derive an Ethereum keypair (ECDSA) try { const evmKeypair = await provider.deriveEcdsaKeypair( - "/path/to/derive", + "/path/to/derive", // This is what the WALLET_SECRET_SALT is used for "subject-identifier", ); // evmKeypair can now be used for Ethereum operations diff --git a/packages/_examples/plugin/src/plugins/samplePlugin.ts b/packages/_examples/plugin/src/plugins/samplePlugin.ts index 90fd2898a16..2a3b29888b2 100644 --- a/packages/_examples/plugin/src/plugins/samplePlugin.ts +++ b/packages/_examples/plugin/src/plugins/samplePlugin.ts @@ -2,6 +2,7 @@ import { Plugin } from "@elizaos/core"; import { createResourceAction } from "../actions/sampleAction"; import { sampleProvider } from "../providers/sampleProvider"; import { sampleEvaluator } from "../evaluators/sampleEvalutor"; +import SampleService from "../services/sampleService"; export const samplePlugin: Plugin = { name: "sample", @@ -10,6 +11,6 @@ export const samplePlugin: Plugin = { providers: [sampleProvider], evaluators: [sampleEvaluator], // separate examples will be added for services and clients - services: [], + services: [new SampleService()], clients: [], }; diff --git a/packages/_examples/plugin/src/services/sampleService.ts b/packages/_examples/plugin/src/services/sampleService.ts new file mode 100644 index 00000000000..07561c5bde5 --- /dev/null +++ b/packages/_examples/plugin/src/services/sampleService.ts @@ -0,0 +1,136 @@ +import { + Service, + ServiceType, + IAgentRuntime, + // Memory, + // State, + elizaLogger, + // stringToUuid, +} from "@elizaos/core"; +// import { sampleProvider } from "../providers/sampleProvider"; // TODO: Uncomment this line to use the sampleProvider + +// Add SAMPLE to ServiceType enum in types.ts +declare module "@elizaos/core" { + export enum ServiceType { + SAMPLE = "sample", + } +} + +// The SampleService is a simple service that logs "Hello world" every 15 minutes. +export class SampleService extends Service { + private runtime: IAgentRuntime | null = null; + private intervalId: NodeJS.Timeout | null = null; + private readonly DEFAULT_INTERVAL = 15 * 60 * 1000; // 15 minutes in milliseconds + + static get serviceType(): ServiceType { + return ServiceType.SAMPLE; + } + + private static isInitialized = false; + + async initialize(runtime: IAgentRuntime): Promise { + // Verify if the service is already initialized + if (SampleService.isInitialized) { + return; + } + + this.runtime = runtime; + + // Start the periodic task + this.startPeriodicTask(); + SampleService.isInitialized = true; + elizaLogger.info("SampleService initialized and started periodic task"); + } + + private static activeTaskCount = 0; + + private startPeriodicTask(): void { + // Verify if a task is already active + if (SampleService.activeTaskCount > 0) { + elizaLogger.warn( + "SampleService: Periodic task already running, skipping" + ); + return; + } + + // Clear any existing interval + if (this.intervalId) { + clearInterval(this.intervalId); + } + + SampleService.activeTaskCount++; + elizaLogger.info( + `SampleService: Starting periodic task (active tasks: ${SampleService.activeTaskCount})` + ); + + // Initial call immediately + this.fetchSample(); + + // Set up periodic calls + this.intervalId = setInterval(() => { + this.fetchSample(); + }, this.DEFAULT_INTERVAL); + } + + private async fetchSample(): Promise { + if (!this.runtime) { + elizaLogger.error("SampleService: Runtime not initialized"); + return; + } + + try { + // Example of using the sampleProvider + // Create dummy memory and state objects for the provider + // const dummyMemory: Memory = { + // id: stringToUuid("sample-service-trigger"), + // userId: this.runtime.agentId, + // agentId: this.runtime.agentId, + // roomId: this.runtime.agentId, + // content: { text: "Periodic sample fetch" }, + // createdAt: Date.now(), + // }; + + // const dummyState: State = { + // userId: this.runtime.agentId, + // bio: "", + // lore: "", + // messageDirections: "", + // postDirections: "", + // roomId: this.runtime.agentId, + // actors: "", + // recentMessages: "", + // recentMessagesData: [], + // }; + // await sampleProvider.get(this.runtime, dummyMemory, dummyState); + + // hello world log example + elizaLogger.info("SampleService: Hello world"); + + elizaLogger.info( + "SampleService: Successfully fetched and processed sample" + ); + } catch (error) { + elizaLogger.error("SampleService: Error fetching sample:", error); + } + } + + // Method to stop the service + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + SampleService.activeTaskCount--; + elizaLogger.info( + `SampleService stopped (active tasks: ${SampleService.activeTaskCount})` + ); + } + SampleService.isInitialized = false; + } + + // Method to manually trigger a sample fetch (for testing) + async forceFetch(): Promise { + await this.fetchSample(); + } +} + +export default SampleService; diff --git a/packages/plugin-evm/src/providers/wallet.ts b/packages/plugin-evm/src/providers/wallet.ts index 50cc2aaabb3..90a3b89b5bf 100644 --- a/packages/plugin-evm/src/providers/wallet.ts +++ b/packages/plugin-evm/src/providers/wallet.ts @@ -306,8 +306,8 @@ export const initWalletProvider = async (runtime: IAgentRuntime) => { const deriveKeyProvider = new DeriveKeyProvider(teeMode); const deriveKeyResult = await deriveKeyProvider.deriveEcdsaKeypair( - "/", walletSecretSalt, + "evm", runtime.agentId ); return new WalletProvider( diff --git a/packages/plugin-gitcoin-passport/README.md b/packages/plugin-gitcoin-passport/README.md new file mode 100644 index 00000000000..80de1caedf4 --- /dev/null +++ b/packages/plugin-gitcoin-passport/README.md @@ -0,0 +1,38 @@ +# `@elizaos/plugin-passport` + +This plugin provides actions for interacting with Gitcoin passport +https://docs.passport.xyz/building-with-passport/passport-api/overview + +--- + +## Installation + +Just add it under your character profile in plugins as + +``` + "plugins": [ + "@elizaos/plugin-gitcoin-passport" + ], +``` + +## Configuration + +Getting Your API Key + +1. Log in to the developer portal: Go to developer.passport.xyz and log in to your account by connecting your wallet. +2. Navigate to the API Keys section: After logging in, go to the "API Keys" section. +3. Generate an API key: Click on the "+ Create a Key" button to generate a unique API key for your account. +4. Store your API key securely: Store your API key in a secure place, as it will be used to access the Passport API. + +Getting your Scorer ID + +1. Log in to the Developer Portal: Go to developer.passport.xyz and log in to your account by connecting your wallet. +2. Navigate to the Scorer section: After logging in, go to the "Scorer" section +3. Create a Scorer: Click on the "+ Create a Scorer" button and input a Scorer name and description. Make sure you use the Unique Humanity scorer, and not the Binary scorer. +4. Find your Scorer ID: Click on the newly created Scorer and you will see the Scorer ID in the page URL. + Example: https://developer.passport.xyz/dashboard/scorer/{scorer_id} + +## Usage + +Results are saved as message and agents can retrive it from there for different use cases. +Default passport treshold of 20 is used, but you can pick your own value and match it agains that diff --git a/packages/plugin-gitcoin-passport/eslint.config.mjs b/packages/plugin-gitcoin-passport/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-gitcoin-passport/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-gitcoin-passport/package.json b/packages/plugin-gitcoin-passport/package.json new file mode 100644 index 00000000000..7e6b183fd1d --- /dev/null +++ b/packages/plugin-gitcoin-passport/package.json @@ -0,0 +1,20 @@ +{ + "name": "@elizaos/plugin-gitcoin-passport", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run", + "lint": "eslint --fix --cache ." + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} \ No newline at end of file diff --git a/packages/plugin-gitcoin-passport/src/actions/getScore.ts b/packages/plugin-gitcoin-passport/src/actions/getScore.ts new file mode 100644 index 00000000000..df9bfff8abf --- /dev/null +++ b/packages/plugin-gitcoin-passport/src/actions/getScore.ts @@ -0,0 +1,137 @@ +import { + Action, + elizaLogger, + IAgentRuntime, + Memory, + HandlerCallback, + State, + getEmbeddingZeroVector, + Content, + composeContext, + generateMessageResponse, + ModelClass, +} from "@elizaos/core"; + +interface PassportScore { + address: string; + score: string; + threshold: string; + passing_score: string; +} + +const createTokenMemory = async ( + runtime: IAgentRuntime, + _message: Memory, + formattedOutput: string +) => { + const memory: Memory = { + userId: _message.userId, + agentId: _message.agentId, + roomId: _message.roomId, + content: { text: formattedOutput }, + createdAt: Date.now(), + embedding: getEmbeddingZeroVector(), + }; + await runtime.messageManager.createMemory(memory); +}; + +export const addressTemplate = `From previous sentence extract only the Ethereum address being asked about. +Respond with a JSON markdown block containing only the extracted value: + +\`\`\`json +{ +"address": string | null +} +\`\`\` +`; + +export const getPassportScoreAction: Action = { + name: "GET_PASSPORT_SCORE", + description: "Get score from Passport API for an address", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + elizaLogger.log("Validating runtime for GET_PASSPORT_SCORE..."); + const apiKey = runtime.getSetting("PASSPORT_API_KEY"); + const scorerId = runtime.getSetting("PASSPORT_SCORER"); + if (!apiKey || !scorerId) { + elizaLogger.error( + "Missing PASSPORT_API_KEY or PASSPORT_SCORER settings" + ); + return false; + } + return true; + }, + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: any, + callback: HandlerCallback + ) => { + elizaLogger.log("Starting GET_PASSPORT_SCORE handler..."); + const apiKey = runtime.getSetting("PASSPORT_API_KEY"); + const scorerId = runtime.getSetting("PASSPORT_SCORER"); + + if (!state) { + state = (await runtime.composeState(_message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + const context = composeContext({ + state, + template: `${_message.content.text}\n${addressTemplate}`, + }); + + const addressRequest = await generateMessageResponse({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); + + const address = addressRequest.address as string; + + if (!address) { + callback({ text: "Address is required." }, []); + return; + } + + try { + const response = await fetch( + `https://api.passport.xyz/v2/stamps/${scorerId}/score/${address}`, + { + method: "GET", + headers: { + "X-API-KEY": apiKey, + accept: "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: PassportScore = await response.json(); + const formattedOutput = `Address: ${data.address}\nScore: ${data.score}${data.passing_score ? "\nScore is above threshold" : `\nScore is below threshold (${data.threshold})`}`; + + await createTokenMemory(runtime, _message, formattedOutput); + + callback({ text: formattedOutput }, []); + } catch (error) { + elizaLogger.error("Error fetching Passport score:", error); + callback( + { + text: "Failed to fetch Passport score. Please check the logs for more details.", + }, + [] + ); + } + }, + examples: [], + similes: [ + "GET_PASSPORT_SCORE", + "FETCH_PASSPORT_SCORE", + "CHECK_PASSPORT_SCORE", + "VIEW_PASSPORT_SCORE", + ], +}; diff --git a/packages/plugin-gitcoin-passport/src/index.ts b/packages/plugin-gitcoin-passport/src/index.ts new file mode 100644 index 00000000000..638affd8c12 --- /dev/null +++ b/packages/plugin-gitcoin-passport/src/index.ts @@ -0,0 +1,15 @@ +export * from "./actions/getScore"; + +import type { Plugin } from "@elizaos/core"; +import { getPassportScoreAction } from "./actions/getScore"; + +export const gitcoinPassportPlugin: Plugin = { + name: "passport", + description: "Gitcoin passport integration plugin", + providers: [], + evaluators: [], + services: [], + actions: [getPassportScoreAction], +}; + +export default gitcoinPassportPlugin; diff --git a/packages/plugin-gitcoin-passport/tsconfig.json b/packages/plugin-gitcoin-passport/tsconfig.json new file mode 100644 index 00000000000..2d8d3fe8181 --- /dev/null +++ b/packages/plugin-gitcoin-passport/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "declaration": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-gitcoin-passport/tsup.config.ts b/packages/plugin-gitcoin-passport/tsup.config.ts new file mode 100644 index 00000000000..a68ccd636ad --- /dev/null +++ b/packages/plugin-gitcoin-passport/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + "@lifi/sdk", + ], +}); diff --git a/packages/plugin-solana/src/keypairUtils.ts b/packages/plugin-solana/src/keypairUtils.ts index 6dbacdb8414..f8405424bc1 100644 --- a/packages/plugin-solana/src/keypairUtils.ts +++ b/packages/plugin-solana/src/keypairUtils.ts @@ -30,8 +30,8 @@ export async function getWalletKey( const deriveKeyProvider = new DeriveKeyProvider(teeMode); const deriveKeyResult = await deriveKeyProvider.deriveEd25519Keypair( - "/", walletSecretSalt, + "solana", runtime.agentId ); diff --git a/packages/plugin-tee/package.json b/packages/plugin-tee/package.json index 58e2d94b7d3..852c29bf9c0 100644 --- a/packages/plugin-tee/package.json +++ b/packages/plugin-tee/package.json @@ -32,7 +32,8 @@ "scripts": { "build": "tsup --format esm --dts", "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache ." + "lint": "eslint --fix --cache .", + "test": "vitest run" }, "peerDependencies": { "whatwg-url": "7.1.0" diff --git a/packages/plugin-tee/src/actions/remoteAttestation.ts b/packages/plugin-tee/src/actions/remoteAttestation.ts index f1fddbdd909..87fa15dde09 100644 --- a/packages/plugin-tee/src/actions/remoteAttestation.ts +++ b/packages/plugin-tee/src/actions/remoteAttestation.ts @@ -1,6 +1,7 @@ import type { IAgentRuntime, Memory, State, HandlerCallback } from "@elizaos/core"; import { RemoteAttestationProvider } from "../providers/remoteAttestationProvider"; import { fetch, type BodyInit } from "undici"; +import { RemoteAttestationMessage } from "../types/tee"; function hexToUint8Array(hex: string) { hex = hex.trim(); @@ -42,23 +43,33 @@ export const remoteAttestationAction = { description: "Generate a remote attestation to prove that the agent is running in a TEE", handler: async ( runtime: IAgentRuntime, - _message: Memory, + message: Memory, _state: State, _options: { [key: string]: unknown }, callback: HandlerCallback, ) => { try { + // Attestation will be generated based on the message info + const attestationMessage: RemoteAttestationMessage = { + agentId: runtime.agentId, + timestamp: Date.now(), + message: { + userId: message.userId, + roomId: message.roomId, + content: message.content.text, + }, + }; // Get the remote attestation of the agentId - const agentId = runtime.agentId; const teeMode = runtime.getSetting("TEE_MODE"); const provider = new RemoteAttestationProvider(teeMode); - const attestation = await provider.generateAttestation(agentId, 'raw'); + + const attestation = await provider.generateAttestation(JSON.stringify(attestationMessage)); const attestationData = hexToUint8Array(attestation.quote); const response = await uploadUint8Array(attestationData); const data = await response.json(); callback({ text: `Here's my 🧾 RA Quote 🫡 - https://proof.t16z.com/reports/${data.checksum}`, +https://proof.t16z.com/reports/${data.checksum}`, action: "NONE", }); return true; diff --git a/packages/plugin-tee/src/providers/deriveKeyProvider.ts b/packages/plugin-tee/src/providers/deriveKeyProvider.ts index 96430f23586..cf6cfd45929 100644 --- a/packages/plugin-tee/src/providers/deriveKeyProvider.ts +++ b/packages/plugin-tee/src/providers/deriveKeyProvider.ts @@ -11,12 +11,7 @@ import { DeriveKeyResponse, TappdClient } from "@phala/dstack-sdk"; import { privateKeyToAccount } from "viem/accounts"; import { PrivateKeyAccount, keccak256 } from "viem"; import { RemoteAttestationProvider } from "./remoteAttestationProvider"; -import { TEEMode, RemoteAttestationQuote } from "../types/tee"; - -interface DeriveKeyAttestationData { - agentId: string; - publicKey: string; -} +import { TEEMode, RemoteAttestationQuote, DeriveKeyAttestationData } from "../types/tee"; class DeriveKeyProvider { private client: TappdClient; @@ -57,11 +52,13 @@ class DeriveKeyProvider { private async generateDeriveKeyAttestation( agentId: string, - publicKey: string + publicKey: string, + subject?: string ): Promise { const deriveKeyData: DeriveKeyAttestationData = { agentId, publicKey, + subject, }; const reportdata = JSON.stringify(deriveKeyData); elizaLogger.log( @@ -72,6 +69,12 @@ class DeriveKeyProvider { return quote; } + /** + * Derives a raw key from the given path and subject. + * @param path - The path to derive the key from. This is used to derive the key from the root of trust. + * @param subject - The subject to derive the key from. This is used for the certificate chain. + * @returns The derived key. + */ async rawDeriveKey( path: string, subject: string @@ -94,6 +97,13 @@ class DeriveKeyProvider { } } + /** + * Derives an Ed25519 keypair from the given path and subject. + * @param path - The path to derive the key from. This is used to derive the key from the root of trust. + * @param subject - The subject to derive the key from. This is used for the certificate chain. + * @param agentId - The agent ID to generate an attestation for. + * @returns An object containing the derived keypair and attestation. + */ async deriveEd25519Keypair( path: string, subject: string, @@ -130,6 +140,13 @@ class DeriveKeyProvider { } } + /** + * Derives an ECDSA keypair from the given path and subject. + * @param path - The path to derive the key from. This is used to derive the key from the root of trust. + * @param subject - The subject to derive the key from. This is used for the certificate chain. + * @param agentId - The agent ID to generate an attestation for. This is used for the certificate chain. + * @returns An object containing the derived keypair and attestation. + */ async deriveEcdsaKeypair( path: string, subject: string, @@ -184,13 +201,13 @@ const deriveKeyProvider: Provider = { const secretSalt = runtime.getSetting("WALLET_SECRET_SALT") || "secret_salt"; const solanaKeypair = await provider.deriveEd25519Keypair( - "/", secretSalt, + "solana", agentId ); const evmKeypair = await provider.deriveEcdsaKeypair( - "/", secretSalt, + "evm", agentId ); return JSON.stringify({ diff --git a/packages/plugin-tee/src/providers/remoteAttestationProvider.ts b/packages/plugin-tee/src/providers/remoteAttestationProvider.ts index 262b58c34f5..fb9c174713d 100644 --- a/packages/plugin-tee/src/providers/remoteAttestationProvider.ts +++ b/packages/plugin-tee/src/providers/remoteAttestationProvider.ts @@ -6,7 +6,7 @@ import { elizaLogger, } from "@elizaos/core"; import { TdxQuoteResponse, TappdClient, TdxQuoteHashAlgorithms } from "@phala/dstack-sdk"; -import { RemoteAttestationQuote, TEEMode } from "../types/tee"; +import { RemoteAttestationQuote, TEEMode, RemoteAttestationMessage } from "../types/tee"; class RemoteAttestationProvider { private client: TappdClient; @@ -74,14 +74,23 @@ class RemoteAttestationProvider { // Keep the original provider for backwards compatibility const remoteAttestationProvider: Provider = { - get: async (runtime: IAgentRuntime, _message: Memory, _state?: State) => { + get: async (runtime: IAgentRuntime, message: Memory, _state?: State) => { const teeMode = runtime.getSetting("TEE_MODE"); const provider = new RemoteAttestationProvider(teeMode); const agentId = runtime.agentId; try { - elizaLogger.log("Generating attestation for: ", agentId); - const attestation = await provider.generateAttestation(agentId, 'raw'); + const attestationMessage: RemoteAttestationMessage = { + agentId: agentId, + timestamp: Date.now(), + message: { + userId: message.userId, + roomId: message.roomId, + content: message.content.text, + } + }; + elizaLogger.log("Generating attestation for: ", JSON.stringify(attestationMessage)); + const attestation = await provider.generateAttestation(JSON.stringify(attestationMessage)); return `Your Agent's remote attestation is: ${JSON.stringify(attestation)}`; } catch (error) { console.error("Error in remote attestation provider:", error); diff --git a/packages/plugin-tee/src/providers/walletProvider.ts b/packages/plugin-tee/src/providers/walletProvider.ts index a31cc65ceac..0d08c0e07bf 100644 --- a/packages/plugin-tee/src/providers/walletProvider.ts +++ b/packages/plugin-tee/src/providers/walletProvider.ts @@ -299,8 +299,8 @@ const walletProvider: Provider = { keypair: Keypair; attestation: RemoteAttestationQuote; } = await deriveKeyProvider.deriveEd25519Keypair( - "/", runtime.getSetting("WALLET_SECRET_SALT"), + "solana", agentId ); publicKey = derivedKeyPair.keypair.publicKey; diff --git a/packages/plugin-tee/src/tests/deriveKey.test.ts b/packages/plugin-tee/src/tests/deriveKey.test.ts new file mode 100644 index 00000000000..4611f1e307b --- /dev/null +++ b/packages/plugin-tee/src/tests/deriveKey.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DeriveKeyProvider } from '../providers/deriveKeyProvider'; +import { TappdClient } from '@phala/dstack-sdk'; +import { TEEMode } from '../types/tee'; + +// Mock dependencies +vi.mock('@phala/dstack-sdk', () => ({ + TappdClient: vi.fn().mockImplementation(() => ({ + deriveKey: vi.fn().mockResolvedValue({ + asUint8Array: () => new Uint8Array([1, 2, 3, 4, 5]) + }), + tdxQuote: vi.fn().mockResolvedValue({ + quote: 'mock-quote-data', + replayRtmrs: () => ['rtmr0', 'rtmr1', 'rtmr2', 'rtmr3'] + }), + rawDeriveKey: vi.fn() + })) +})); + +vi.mock('@solana/web3.js', () => ({ + Keypair: { + fromSeed: vi.fn().mockReturnValue({ + publicKey: { + toBase58: () => 'mock-solana-public-key' + } + }) + } +})); + +vi.mock('viem/accounts', () => ({ + privateKeyToAccount: vi.fn().mockReturnValue({ + address: 'mock-evm-address' + }) +})); + +describe('DeriveKeyProvider', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with LOCAL mode', () => { + const _provider = new DeriveKeyProvider(TEEMode.LOCAL); + expect(TappdClient).toHaveBeenCalledWith('http://localhost:8090'); + }); + + it('should initialize with DOCKER mode', () => { + const _provider = new DeriveKeyProvider(TEEMode.DOCKER); + expect(TappdClient).toHaveBeenCalledWith('http://host.docker.internal:8090'); + }); + + it('should initialize with PRODUCTION mode', () => { + const _provider = new DeriveKeyProvider(TEEMode.PRODUCTION); + expect(TappdClient).toHaveBeenCalledWith(); + }); + + it('should throw error for invalid mode', () => { + expect(() => new DeriveKeyProvider('INVALID_MODE')).toThrow('Invalid TEE_MODE'); + }); + }); + + describe('rawDeriveKey', () => { + let _provider: DeriveKeyProvider; + + beforeEach(() => { + _provider = new DeriveKeyProvider(TEEMode.LOCAL); + }); + + it('should derive raw key successfully', async () => { + const path = 'test-path'; + const subject = 'test-subject'; + const result = await _provider.rawDeriveKey(path, subject); + + const client = TappdClient.mock.results[0].value; + expect(client.deriveKey).toHaveBeenCalledWith(path, subject); + expect(result.asUint8Array()).toEqual(new Uint8Array([1, 2, 3, 4, 5])); + }); + + it('should handle errors during raw key derivation', async () => { + const mockError = new Error('Key derivation failed'); + vi.mocked(TappdClient).mockImplementationOnce(() => { + const instance = new TappdClient(); + instance.deriveKey = vi.fn().mockRejectedValueOnce(mockError); + instance.tdxQuote = vi.fn(); + instance.rawDeriveKey = vi.fn(); + return instance; + }); + + const provider = new DeriveKeyProvider(TEEMode.LOCAL); + await expect(provider.rawDeriveKey('path', 'subject')).rejects.toThrow(mockError); + }); + }); + + describe('deriveEd25519Keypair', () => { + let provider: DeriveKeyProvider; + + beforeEach(() => { + provider = new DeriveKeyProvider(TEEMode.LOCAL); + }); + + it('should derive Ed25519 keypair successfully', async () => { + const path = 'test-path'; + const subject = 'test-subject'; + const agentId = 'test-agent'; + + const result = await provider.deriveEd25519Keypair(path, subject, agentId); + + expect(result).toHaveProperty('keypair'); + expect(result).toHaveProperty('attestation'); + expect(result.keypair.publicKey.toBase58()).toBe('mock-solana-public-key'); + }); + }); + + describe('deriveEcdsaKeypair', () => { + let provider: DeriveKeyProvider; + + beforeEach(() => { + provider = new DeriveKeyProvider(TEEMode.LOCAL); + }); + + it('should derive ECDSA keypair successfully', async () => { + const path = 'test-path'; + const subject = 'test-subject'; + const agentId = 'test-agent'; + + const result = await provider.deriveEcdsaKeypair(path, subject, agentId); + + expect(result).toHaveProperty('keypair'); + expect(result).toHaveProperty('attestation'); + expect(result.keypair.address).toBe('mock-evm-address'); + }); + }); +}); \ No newline at end of file diff --git a/packages/plugin-tee/src/tests/remoteAttestation.test.ts b/packages/plugin-tee/src/tests/remoteAttestation.test.ts new file mode 100644 index 00000000000..d9c401f2568 --- /dev/null +++ b/packages/plugin-tee/src/tests/remoteAttestation.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { RemoteAttestationProvider } from '../providers/remoteAttestationProvider'; +import { TappdClient } from '@phala/dstack-sdk'; +import { TEEMode } from '../types/tee'; + +// Mock TappdClient +vi.mock('@phala/dstack-sdk', () => ({ + TappdClient: vi.fn().mockImplementation(() => ({ + tdxQuote: vi.fn().mockResolvedValue({ + quote: 'mock-quote-data', + replayRtmrs: () => ['rtmr0', 'rtmr1', 'rtmr2', 'rtmr3'] + }), + deriveKey: vi.fn() + })) +})); + +describe('RemoteAttestationProvider', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should initialize with LOCAL mode', () => { + const _provider = new RemoteAttestationProvider(TEEMode.LOCAL); + expect(TappdClient).toHaveBeenCalledWith('http://localhost:8090'); + }); + + it('should initialize with DOCKER mode', () => { + const _provider = new RemoteAttestationProvider(TEEMode.DOCKER); + expect(TappdClient).toHaveBeenCalledWith('http://host.docker.internal:8090'); + }); + + it('should initialize with PRODUCTION mode', () => { + const _provider = new RemoteAttestationProvider(TEEMode.PRODUCTION); + expect(TappdClient).toHaveBeenCalledWith(); + }); + + it('should throw error for invalid mode', () => { + expect(() => new RemoteAttestationProvider('INVALID_MODE')).toThrow('Invalid TEE_MODE'); + }); + }); + + describe('generateAttestation', () => { + let provider: RemoteAttestationProvider; + + beforeEach(() => { + provider = new RemoteAttestationProvider(TEEMode.LOCAL); + }); + + it('should generate attestation successfully', async () => { + const reportData = 'test-report-data'; + const quote = await provider.generateAttestation(reportData); + + expect(quote).toEqual({ + quote: 'mock-quote-data', + timestamp: expect.any(Number) + }); + }); + + it('should handle errors during attestation generation', async () => { + const mockError = new Error('TDX Quote generation failed'); + const mockTdxQuote = vi.fn().mockRejectedValue(mockError); + vi.mocked(TappdClient).mockImplementationOnce(() => ({ + tdxQuote: mockTdxQuote, + deriveKey: vi.fn() + })); + + const provider = new RemoteAttestationProvider(TEEMode.LOCAL); + await expect(provider.generateAttestation('test-data')).rejects.toThrow('Failed to generate TDX Quote'); + }); + + it('should pass hash algorithm to tdxQuote when provided', async () => { + const reportData = 'test-report-data'; + const hashAlgorithm = 'raw'; + await provider.generateAttestation(reportData, hashAlgorithm); + + const client = TappdClient.mock.results[0].value; + expect(client.tdxQuote).toHaveBeenCalledWith(reportData, hashAlgorithm); + }); + }); +}); \ No newline at end of file diff --git a/packages/plugin-tee/src/tests/remoteAttestationAction.test.ts b/packages/plugin-tee/src/tests/remoteAttestationAction.test.ts new file mode 100644 index 00000000000..3cec5f7d3b9 --- /dev/null +++ b/packages/plugin-tee/src/tests/remoteAttestationAction.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { remoteAttestationAction } from '../actions/remoteAttestation'; +import { RemoteAttestationProvider } from '../providers/remoteAttestationProvider'; + +// Mock dependencies +vi.mock('../providers/remoteAttestationProvider'); +vi.mock('undici', () => ({ + fetch: vi.fn().mockResolvedValue({ + json: () => Promise.resolve({ checksum: 'mock-checksum' }) + }) +})); + +describe('remoteAttestationAction', () => { + const mockRuntime = { + agentId: 'test-agent-id', + getSetting: vi.fn().mockReturnValue('LOCAL'), + getState: vi.fn(), + setState: vi.fn(), + message:{ userId: 'user', roomId: 'room', content: { text: 'If you are running in a TEE, generate a remote attestation' } }, + setConversation: vi.fn() + }; + + const mockCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(RemoteAttestationProvider).mockImplementation(() => ({ + generateAttestation: vi.fn().mockResolvedValue({ + quote: '0x1234', + timestamp: Date.now() + }) + })); + }); + + it('should have correct name and similes', () => { + expect(remoteAttestationAction.name).toBe('REMOTE_ATTESTATION'); + expect(remoteAttestationAction.similes).toContain('REMOTE_ATTESTATION'); + expect(remoteAttestationAction.similes).toContain('TEE_REMOTE_ATTESTATION'); + expect(remoteAttestationAction.similes).toContain('TEE_ATTESTATION'); + }); + + describe('handler', () => { + it('should generate and upload attestation successfully', async () => { + const result = await remoteAttestationAction.handler( + mockRuntime, + mockRuntime.message, + {}, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('https://proof.t16z.com/reports/mock-checksum'), + action: 'NONE' + }); + }); + + it('should handle errors during attestation generation', async () => { + const mockError = new Error('Attestation generation failed'); + vi.mocked(RemoteAttestationProvider).mockImplementation(() => ({ + generateAttestation: vi.fn().mockRejectedValueOnce(mockError), + client: { + tdxQuote: vi.fn(), + deriveKey: vi.fn() + } + })); + + const result = await remoteAttestationAction.handler( + mockRuntime, + {}, + {}, + {}, + mockCallback + ); + + expect(result).toBe(false); + }); + + }); + + describe('validate', () => { + it('should always return true', async () => { + const result = await remoteAttestationAction.validate(mockRuntime); + expect(result).toBe(true); + }); + }); + + describe('examples', () => { + it('should have valid example conversations', () => { + expect(remoteAttestationAction.examples).toBeInstanceOf(Array); + expect(remoteAttestationAction.examples[0]).toBeInstanceOf(Array); + + const [userMessage, agentMessage] = remoteAttestationAction.examples[0]; + expect(userMessage.user).toBe('{{user1}}'); + expect(userMessage.content.text).toBe('If you are running in a TEE, generate a remote attestation'); + + expect(agentMessage.user).toBe('{{user2}}'); + expect(agentMessage.content.text).toBe('Of course, one second...'); + expect(agentMessage.content.action).toBe('REMOTE_ATTESTATION'); + }); + }); +}); \ No newline at end of file diff --git a/packages/plugin-tee/src/types/tee.ts b/packages/plugin-tee/src/types/tee.ts index de974c39243..9e3ae1fed5c 100644 --- a/packages/plugin-tee/src/types/tee.ts +++ b/packages/plugin-tee/src/types/tee.ts @@ -9,3 +9,19 @@ export interface RemoteAttestationQuote { quote: string; timestamp: number; } + +export interface DeriveKeyAttestationData { + agentId: string; + publicKey: string; + subject?: string; +} + +export interface RemoteAttestationMessage { + agentId: string; + timestamp: number; + message: { + userId: string; + roomId: string; + content: string; + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 271240f250a..a252b4b0d0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: '@elizaos/plugin-gitbook': specifier: workspace:* version: link:../packages/plugin-gitbook + '@elizaos/plugin-gitcoin-passport': + specifier: workspace:* + version: link:../packages/plugin-gitcoin-passport '@elizaos/plugin-goat': specifier: workspace:* version: link:../packages/plugin-goat @@ -1878,6 +1881,18 @@ importers: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + packages/plugin-gitcoin-passport: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.0)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-goat: dependencies: '@elizaos/core':