@elizaos/core 2.0.0-alpha.336 → 2.0.0-alpha.338
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/dist/browser/index.browser.js.map +1 -1
- package/dist/browser/roles.js.map +1 -1
- package/dist/edge/index.edge.js.map +1 -1
- package/dist/node/features/advanced-capabilities/clipboard/index.js.map +1 -1
- package/dist/node/index.node.js.map +1 -1
- package/dist/node/roles.js.map +1 -1
- package/dist/types/trigger.d.ts +4 -2
- package/dist/types/trigger.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -457,7 +457,7 @@
|
|
|
457
457
|
"import type { Metadata } from \"./primitives\";\nimport type { JsonValue } from \"./proto.js\";\nimport type { IAgentRuntime } from \"./runtime\";\n\n/**\n * Core service type registry that can be extended by plugins via module augmentation.\n * Plugins can extend this interface to add their own service types:\n *\n * @example\n * ```typescript\n * declare module '@elizaos/core' {\n * interface ServiceTypeRegistry {\n * MY_CUSTOM_SERVICE: 'my_custom_service';\n * }\n * }\n * ```\n */\nexport interface ServiceTypeRegistry {\n\tTRANSCRIPTION: \"transcription\";\n\tVIDEO: \"video\";\n\tBROWSER: \"browser\";\n\tPDF: \"pdf\";\n\tREMOTE_FILES: \"aws_s3\";\n\tWEB_SEARCH: \"web_search\";\n\tEMAIL: \"email\";\n\tTEE: \"tee\";\n\tTASK: \"task\";\n\tAPPROVAL: \"approval\";\n\tTOOL_POLICY: \"tool_policy\";\n\tWALLET: \"wallet\";\n\tLP_POOL: \"lp_pool\";\n\tTOKEN_DATA: \"token_data\";\n\tMESSAGE_SERVICE: \"message_service\";\n\tMESSAGE: \"message\";\n\tPOST: \"post\";\n\tHOOKS: \"hooks\";\n\tPAIRING: \"pairing\";\n\tAGENT_EVENT: \"agent_event\";\n\tOPTIMIZED_PROMPT: \"optimized_prompt\";\n\tUNKNOWN: \"unknown\";\n}\n\n/**\n * Type for service names that includes both core services and any plugin-registered services\n */\nexport type ServiceTypeName = ServiceTypeRegistry[keyof ServiceTypeRegistry];\n\n/**\n * Helper type to extract service type values from the registry\n */\nexport type ServiceTypeValue<K extends keyof ServiceTypeRegistry> =\n\tServiceTypeRegistry[K];\n\n/**\n * Helper type to check if a service type exists in the registry\n */\nexport type IsValidServiceType<T extends string> = T extends ServiceTypeName\n\t? true\n\t: false;\n\n/**\n * Type-safe service class definition\n */\nexport type TypedServiceClass<T extends ServiceTypeName> = {\n\tnew (runtime?: IAgentRuntime): Service;\n\tserviceType: T;\n\tstart(runtime: IAgentRuntime): Promise<Service>;\n};\n\n/**\n * Map of service type names to their implementation classes.\n * Plugins can extend this via module augmentation:\n * @example\n * ```typescript\n * declare module '@elizaos/core' {\n * interface ServiceClassMap {\n * MY_SERVICE: typeof MyService;\n * }\n * }\n * ```\n */\n// biome-ignore lint/complexity/noBannedTypes: Empty interface for module augmentation\nexport type ServiceClassMap = {};\n\n/**\n * Helper to infer service instance type from service type name\n */\nexport type ServiceInstance<T extends ServiceTypeName> =\n\tT extends keyof ServiceClassMap ? InstanceType<ServiceClassMap[T]> : Service;\n\n/**\n * Runtime service registry type\n */\nexport type ServiceRegistry<T extends ServiceTypeName = ServiceTypeName> = Map<\n\tT,\n\tService\n>;\n\n/**\n * Enumerates the recognized types of services that can be registered and used by the agent runtime.\n * Services provide specialized functionalities like audio transcription, video processing,\n * web browsing, PDF handling, file storage (e.g., AWS S3), web search, email integration,\n * secure execution via TEE (Trusted Execution Environment), and task management.\n * This constant is used in `AgentRuntime` for service registration and retrieval (e.g., `getService`).\n * Each service typically implements the `Service` abstract class or a more specific interface like `IVideoService`.\n */\nexport const ServiceType = {\n\tTRANSCRIPTION: \"transcription\",\n\tVIDEO: \"video\",\n\tBROWSER: \"browser\",\n\tPDF: \"pdf\",\n\tREMOTE_FILES: \"aws_s3\",\n\tWEB_SEARCH: \"web_search\",\n\tEMAIL: \"email\",\n\tTEE: \"tee\",\n\tTASK: \"task\",\n\tAPPROVAL: \"approval\",\n\tTOOL_POLICY: \"tool_policy\",\n\tWALLET: \"wallet\",\n\tLP_POOL: \"lp_pool\",\n\tTOKEN_DATA: \"token_data\",\n\tMESSAGE_SERVICE: \"message_service\",\n\tMESSAGE: \"message\",\n\tPOST: \"post\",\n\tHOOKS: \"hooks\",\n\tPAIRING: \"pairing\",\n\tAGENT_EVENT: \"agent_event\",\n\tVOICE_CACHE: \"voice_cache\",\n\tOPTIMIZED_PROMPT: \"optimized_prompt\",\n\tUNKNOWN: \"unknown\",\n} as const;\n\n/**\n * Client instance\n */\nexport abstract class Service {\n\t/** Runtime instance */\n\tprotected runtime!: IAgentRuntime;\n\n\tconstructor(runtime?: IAgentRuntime) {\n\t\tif (runtime) {\n\t\t\tthis.runtime = runtime;\n\t\t}\n\t}\n\n\tabstract stop(): Promise<void>;\n\n\t/** Service type */\n\tstatic serviceType: string;\n\n\t/** Service name */\n\tabstract capabilityDescription: string;\n\n\t/** Service configuration */\n\tconfig?: Metadata;\n\n\t/** Start service connection - subclasses must override this */\n\tstatic async start(_runtime: IAgentRuntime): Promise<Service> {\n\t\tthrow new Error(\"Service.start() must be implemented by subclass\");\n\t}\n\n\t/** Stop service connection - optional, subclasses may override this */\n\tstatic stopRuntime?(_runtime: IAgentRuntime): Promise<void>;\n\n\t/** Optional static method to register send handlers */\n\tstatic registerSendHandlers?(runtime: IAgentRuntime, service: Service): void;\n}\n\n/**\n * Generic service interface that provides better type checking for services\n * @template ConfigType The configuration type for this service\n * @template InputType The input type for processing\n * @template ResultType The result type returned by the service operations\n */\nexport interface TypedService<\n\tConfigType extends Metadata = Metadata,\n\tInputType = JsonValue,\n\tResultType = JsonValue,\n> extends Service {\n\t/**\n\t * The configuration for this service instance\n\t */\n\tconfig?: ConfigType;\n\n\t/**\n\t * Process an input with this service\n\t * @param input The input to process\n\t * @returns A promise resolving to the result\n\t */\n\tprocess(input: InputType): Promise<ResultType>;\n}\n\n/**\n * Generic factory function to create a typed service instance.\n * getService() is synchronous — no await needed.\n * @param runtime The agent runtime\n * @param serviceType The type of service to get\n * @returns The service instance or null if not available\n */\nexport function getTypedService<\n\tConfigType extends Metadata = Metadata,\n\tInputType = JsonValue,\n\tResultType = JsonValue,\n>(\n\truntime: IAgentRuntime,\n\tserviceType: ServiceTypeName,\n): TypedService<ConfigType, InputType, ResultType> | null {\n\treturn runtime.getService<TypedService<ConfigType, InputType, ResultType>>(\n\t\tserviceType,\n\t);\n}\n\n/**\n * Standardized service error type for consistent error handling\n */\nexport interface ServiceError {\n\tcode: string;\n\tmessage: string;\n\tdetails?: Record<string, JsonValue> | string | number | boolean | null;\n\tcause?: Error;\n}\n\n/**\n * Safely create a ServiceError from any caught error\n */\nexport function createServiceError(\n\terror: Error | string | JsonValue,\n\tcode = \"UNKNOWN_ERROR\",\n): ServiceError {\n\tif (error instanceof Error) {\n\t\treturn {\n\t\t\tcode,\n\t\t\tmessage: error.message,\n\t\t\tcause: error,\n\t\t};\n\t}\n\n\treturn {\n\t\tcode,\n\t\tmessage: String(error),\n\t};\n}\n",
|
|
458
458
|
"/**\n * Service Interface Definitions for elizaOS\n *\n * This module provides standardized service interface definitions that plugins implement.\n * Data types are proto-generated; runtime classes remain TypeScript.\n */\n\nimport type { Content, UUID } from \"./primitives\";\nimport type {\n\tJsonValue,\n\tLpPositionDetails,\n\tPoolInfo,\n\tTokenBalance,\n\tTokenData,\n\tTransactionResult,\n\tWalletAsset,\n\tWalletPortfolio,\n} from \"./proto.js\";\nimport { Service, ServiceType } from \"./service\";\n\nexport type {\n\tLpPositionDetails,\n\tPoolInfo,\n\tTokenBalance,\n\tTokenData,\n\tTransactionResult,\n\tWalletAsset,\n\tWalletPortfolio,\n};\n\n// ============================================================================\n// Message Bus Service Interface\n// ============================================================================\n\nexport interface IMessageBusService extends Service {\n\tnotifyActionStart(\n\t\troomId: UUID,\n\t\tworldId: UUID,\n\t\tcontent: Content,\n\t\tmessageId?: UUID,\n\t): Promise<void>;\n\n\tnotifyActionUpdate(\n\t\troomId: UUID,\n\t\tworldId: UUID,\n\t\tcontent: Content,\n\t\tmessageId?: UUID,\n\t): Promise<void>;\n}\n\n// ============================================================================\n// Token & Wallet Interfaces\n// ============================================================================\n\nexport abstract class ITokenDataService extends Service {\n\tstatic override readonly serviceType = ServiceType.TOKEN_DATA;\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to token market data.\" as string;\n\n\tabstract getTokenDetails(\n\t\taddress: string,\n\t\tchain: string,\n\t): Promise<TokenData | null>;\n\n\tabstract getTrendingTokens(\n\t\tchain?: string,\n\t\tlimit?: number,\n\t\ttimePeriod?: string,\n\t): Promise<TokenData[]>;\n\n\tabstract searchTokens(\n\t\tquery: string,\n\t\tchain?: string,\n\t\tlimit?: number,\n\t): Promise<TokenData[]>;\n\n\tabstract getTokensByAddresses(\n\t\taddresses: string[],\n\t\tchain: string,\n\t): Promise<TokenData[]>;\n}\n\nexport abstract class IWalletService extends Service {\n\tstatic override readonly serviceType = ServiceType.WALLET;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to wallet balances and portfolios.\";\n\n\tabstract getPortfolio(owner?: string): Promise<WalletPortfolio>;\n\n\tabstract getBalance(assetAddress: string, owner?: string): Promise<number>;\n\n\tabstract transferSol(\n\t\tfrom: object,\n\t\tto: object,\n\t\tlamports: number,\n\t): Promise<string>;\n}\n\n// ============================================================================\n// Liquidity Pool Interfaces\n// ============================================================================\n\nexport abstract class ILpService extends Service {\n\tstatic override readonly serviceType = \"lp_pool\";\n\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to DEX liquidity pools.\";\n\n\tabstract getDexName(): string;\n\n\tabstract getPools(\n\t\ttokenAMint?: string,\n\t\ttokenBMint?: string,\n\t): Promise<PoolInfo[]>;\n\n\tabstract addLiquidity(params: {\n\t\tuserVault: object;\n\t\tpoolId: string;\n\t\ttokenAAmountLamports: string;\n\t\ttokenBAmountLamports?: string;\n\t\tslippageBps: number;\n\t\ttickLowerIndex?: number;\n\t\ttickUpperIndex?: number;\n\t}): Promise<TransactionResult & { lpTokensReceived?: TokenBalance }>;\n\n\tabstract removeLiquidity(params: {\n\t\tuserVault: object;\n\t\tpoolId: string;\n\t\tlpTokenAmountLamports: string;\n\t\tslippageBps: number;\n\t}): Promise<TransactionResult & { tokensReceived?: TokenBalance[] }>;\n\n\tabstract getLpPositionDetails(\n\t\tuserAccountPublicKey: string,\n\t\tpoolOrPositionIdentifier: string,\n\t): Promise<LpPositionDetails | null>;\n\n\tabstract getMarketDataForPools(\n\t\tpoolIds: string[],\n\t): Promise<Record<string, Partial<PoolInfo>>>;\n}\n\n// ============================================================================\n// Transcription & Audio Interfaces\n// ============================================================================\n\nexport abstract class ITranscriptionService extends Service {\n\tstatic override readonly serviceType = ServiceType.TRANSCRIPTION;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Audio transcription and speech processing capabilities\";\n\n\tabstract transcribeAudio(\n\t\taudioPath: string | Buffer,\n\t\toptions?: TranscriptionOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract transcribeVideo(\n\t\tvideoPath: string | Buffer,\n\t\toptions?: TranscriptionOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract speechToText(\n\t\taudioStream: NodeJS.ReadableStream | Buffer,\n\t\toptions?: SpeechToTextOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract textToSpeech(\n\t\ttext: string,\n\t\toptions?: TextToSpeechOptions,\n\t): Promise<Buffer>;\n\n\tabstract getSupportedLanguages(): Promise<string[]>;\n\n\tabstract getAvailableVoices(): Promise<\n\t\tArray<{\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tlanguage: string;\n\t\t\tgender?: \"male\" | \"female\" | \"neutral\";\n\t\t}>\n\t>;\n\n\tabstract detectLanguage(audioPath: string | Buffer): Promise<string>;\n}\n\n// ============================================================================\n// Video Interfaces\n// ============================================================================\n\nexport abstract class IVideoService extends Service {\n\tstatic override readonly serviceType = ServiceType.VIDEO;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Video download, processing, and conversion capabilities\";\n\n\tabstract getVideoInfo(url: string): Promise<VideoInfo>;\n\n\tabstract downloadVideo(\n\t\turl: string,\n\t\toptions?: VideoDownloadOptions,\n\t): Promise<string>;\n\n\tabstract extractAudio(\n\t\tvideoPath: string,\n\t\toutputPath?: string,\n\t): Promise<string>;\n\n\tabstract getThumbnail(videoPath: string, timestamp?: number): Promise<string>;\n\n\tabstract convertVideo(\n\t\tvideoPath: string,\n\t\toutputPath: string,\n\t\toptions?: VideoProcessingOptions,\n\t): Promise<string>;\n\n\tabstract getAvailableFormats(url: string): Promise<VideoFormat[]>;\n}\n\n// ============================================================================\n// Browser Interfaces\n// ============================================================================\n\nexport abstract class IBrowserService extends Service {\n\tstatic override readonly serviceType = ServiceType.BROWSER;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Web browser automation and scraping capabilities\";\n\n\tabstract navigate(\n\t\turl: string,\n\t\toptions?: BrowserNavigationOptions,\n\t): Promise<void>;\n\n\tabstract screenshot(options?: ScreenshotOptions): Promise<Buffer>;\n\n\tabstract extractContent(selector?: string): Promise<ExtractedContent>;\n\n\tabstract click(\n\t\tselector: string | ElementSelector,\n\t\toptions?: ClickOptions,\n\t): Promise<void>;\n\n\tabstract type(\n\t\tselector: string,\n\t\ttext: string,\n\t\toptions?: TypeOptions,\n\t): Promise<void>;\n\n\tabstract waitForElement(selector: string | ElementSelector): Promise<void>;\n\n\tabstract evaluate<T = JsonValue>(\n\t\tscript: string,\n\t\t...args: JsonValue[]\n\t): Promise<T>;\n\n\tabstract getCurrentUrl(): Promise<string>;\n\n\tabstract goBack(): Promise<void>;\n\n\tabstract goForward(): Promise<void>;\n\n\tabstract refresh(): Promise<void>;\n}\n\n// ============================================================================\n// PDF Interfaces\n// ============================================================================\n\nexport abstract class IPdfService extends Service {\n\tstatic override readonly serviceType = ServiceType.PDF;\n\n\tpublic readonly capabilityDescription =\n\t\t\"PDF processing, extraction, and generation capabilities\";\n\n\tabstract extractText(pdfPath: string | Buffer): Promise<PdfExtractionResult>;\n\n\tabstract generatePdf(\n\t\thtmlContent: string,\n\t\toptions?: PdfGenerationOptions,\n\t): Promise<Buffer>;\n\n\tabstract convertToPdf(\n\t\tfilePath: string,\n\t\toptions?: PdfConversionOptions,\n\t): Promise<Buffer>;\n\n\tabstract mergePdfs(pdfPaths: (string | Buffer)[]): Promise<Buffer>;\n\n\tabstract splitPdf(pdfPath: string | Buffer): Promise<Buffer[]>;\n}\n\n// ============================================================================\n// Web Search Interfaces\n// ============================================================================\n\nexport abstract class IWebSearchService extends Service {\n\tstatic override readonly serviceType = ServiceType.WEB_SEARCH;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Web search and content discovery capabilities\";\n\n\tabstract search(\n\t\tquery: string,\n\t\toptions?: SearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchNews(\n\t\tquery: string,\n\t\toptions?: NewsSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchImages(\n\t\tquery: string,\n\t\toptions?: ImageSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchVideos(\n\t\tquery: string,\n\t\toptions?: VideoSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract getSuggestions(query: string): Promise<string[]>;\n\n\tabstract getTrendingSearches(region?: string): Promise<string[]>;\n\n\tabstract getPageInfo(url: string): Promise<{\n\t\ttitle: string;\n\t\tdescription: string;\n\t\tcontent: string;\n\t\tmetadata: Record<string, string>;\n\t\timages: string[];\n\t\tlinks: string[];\n\t}>;\n}\n\n// ============================================================================\n// Email Interfaces\n// ============================================================================\n\nexport abstract class IEmailService extends Service {\n\tstatic override readonly serviceType = ServiceType.EMAIL;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Email sending, receiving, and management capabilities\";\n\n\tabstract sendEmail(\n\t\tmessage: EmailMessage,\n\t\toptions?: EmailSendOptions,\n\t): Promise<string>;\n\n\tabstract getEmails(options?: EmailSearchOptions): Promise<EmailMessage[]>;\n\n\tabstract getEmail(messageId: string): Promise<EmailMessage>;\n\n\tabstract deleteEmail(messageId: string): Promise<void>;\n\n\tabstract markEmailAsRead(messageId: string, read: boolean): Promise<void>;\n\n\tabstract flagEmail(messageId: string, flagged: boolean): Promise<void>;\n\n\tabstract moveEmail(messageId: string, folderPath: string): Promise<void>;\n\n\tabstract getFolders(): Promise<EmailFolder[]>;\n\n\tabstract createFolder(folderName: string, parentPath?: string): Promise<void>;\n\n\tabstract getAccountInfo(): Promise<EmailAccount>;\n\n\tabstract searchEmails(\n\t\tquery: string,\n\t\toptions?: EmailSearchOptions,\n\t): Promise<EmailMessage[]>;\n}\n\n// ============================================================================\n// Message Interfaces\n// ============================================================================\n\nexport abstract class IMessagingService extends Service {\n\tstatic override readonly serviceType = ServiceType.MESSAGE;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Platform messaging and channel management capabilities\";\n\n\tabstract sendMessage(\n\t\tchannelId: UUID,\n\t\tcontent: MessageContent,\n\t\toptions?: MessageSendOptions,\n\t): Promise<UUID>;\n\n\tabstract getMessages(\n\t\tchannelId: UUID,\n\t\toptions?: MessageSearchOptions,\n\t): Promise<MessageInfo[]>;\n\n\tabstract getMessage(messageId: UUID): Promise<MessageInfo>;\n\n\tabstract editMessage(messageId: UUID, content: MessageContent): Promise<void>;\n\n\tabstract deleteMessage(messageId: UUID): Promise<void>;\n\n\tabstract addReaction(messageId: UUID, emoji: string): Promise<void>;\n\n\tabstract removeReaction(messageId: UUID, emoji: string): Promise<void>;\n\n\tabstract pinMessage(messageId: UUID): Promise<void>;\n\n\tabstract unpinMessage(messageId: UUID): Promise<void>;\n\n\tabstract getChannels(): Promise<MessageChannel[]>;\n\n\tabstract getChannel(channelId: UUID): Promise<MessageChannel>;\n\n\tabstract createChannel(\n\t\tname: string,\n\t\ttype: MessageChannel[\"type\"],\n\t\toptions?: {\n\t\t\tdescription?: string;\n\t\t\tparticipants?: UUID[];\n\t\t\tprivate?: boolean;\n\t\t},\n\t): Promise<UUID>;\n\n\tabstract searchMessages(\n\t\tquery: string,\n\t\toptions?: MessageSearchOptions,\n\t): Promise<MessageInfo[]>;\n}\n\n// ============================================================================\n// Post/Social Media Interfaces\n// ============================================================================\n\nexport abstract class IPostService extends Service {\n\tstatic override readonly serviceType = ServiceType.POST;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Social media posting and content management capabilities\";\n\n\tabstract createPost(\n\t\tcontent: PostContent,\n\t\toptions?: PostCreateOptions,\n\t): Promise<UUID>;\n\n\tabstract getPosts(options?: PostSearchOptions): Promise<PostInfo[]>;\n\n\tabstract getPost(postId: UUID): Promise<PostInfo>;\n\n\tabstract editPost(postId: UUID, content: PostContent): Promise<void>;\n\n\tabstract deletePost(postId: UUID): Promise<void>;\n\n\tabstract likePost(postId: UUID, like: boolean): Promise<void>;\n\n\tabstract sharePost(postId: UUID, comment?: string): Promise<UUID>;\n\n\tabstract savePost(postId: UUID, save: boolean): Promise<void>;\n\n\tabstract commentOnPost(postId: UUID, content: PostContent): Promise<UUID>;\n\n\tabstract getComments(\n\t\tpostId: UUID,\n\t\toptions?: PostSearchOptions,\n\t): Promise<PostInfo[]>;\n\n\tabstract schedulePost(\n\t\tcontent: PostContent,\n\t\tscheduledAt: Date,\n\t\toptions?: PostCreateOptions,\n\t): Promise<UUID>;\n\n\tabstract getPostAnalytics(postId: UUID): Promise<PostAnalytics>;\n\n\tabstract getTrendingPosts(options?: PostSearchOptions): Promise<PostInfo[]>;\n\n\tabstract searchPosts(\n\t\tquery: string,\n\t\toptions?: PostSearchOptions,\n\t): Promise<PostInfo[]>;\n}\n\n// ============================================================================\n// Transcription & Audio Interfaces\n// ============================================================================\n\n/**\n * Options for audio transcription.\n */\nexport interface TranscriptionOptions {\n\t/** Language code for transcription */\n\tlanguage?: string;\n\t/** Model to use for transcription */\n\tmodel?: string;\n\t/** Temperature for generation */\n\ttemperature?: number;\n\t/** Prompt to guide transcription */\n\tprompt?: string;\n\t/** Response format */\n\tresponse_format?: \"json\" | \"text\" | \"srt\" | \"vtt\" | \"verbose_json\";\n\t/** Timestamp granularities to include */\n\ttimestamp_granularities?: (\"word\" | \"segment\")[];\n\t/** Include word-level timestamps */\n\tword_timestamps?: boolean;\n\t/** Include segment-level timestamps */\n\tsegment_timestamps?: boolean;\n}\n\n/**\n * Result of audio transcription.\n */\nexport interface TranscriptionResult {\n\t/** Transcribed text */\n\ttext: string;\n\t/** Detected language */\n\tlanguage?: string;\n\t/** Audio duration in seconds */\n\tduration?: number;\n\t/** Transcription segments */\n\tsegments?: TranscriptionSegment[];\n\t/** Word-level transcription */\n\twords?: TranscriptionWord[];\n\t/** Overall confidence score */\n\tconfidence?: number;\n}\n\n/**\n * A segment of transcription.\n */\nexport interface TranscriptionSegment {\n\t/** Segment ID */\n\tid: number;\n\t/** Segment text */\n\ttext: string;\n\t/** Start time in seconds */\n\tstart: number;\n\t/** End time in seconds */\n\tend: number;\n\t/** Confidence score */\n\tconfidence?: number;\n\t/** Token IDs */\n\ttokens?: number[];\n\t/** Temperature used */\n\ttemperature?: number;\n\t/** Average log probability */\n\tavg_logprob?: number;\n\t/** Compression ratio */\n\tcompression_ratio?: number;\n\t/** No speech probability */\n\tno_speech_prob?: number;\n}\n\n/**\n * A word in transcription.\n */\nexport interface TranscriptionWord {\n\t/** The word */\n\tword: string;\n\t/** Start time in seconds */\n\tstart: number;\n\t/** End time in seconds */\n\tend: number;\n\t/** Confidence score */\n\tconfidence?: number;\n}\n\n/**\n * Options for speech-to-text.\n */\nexport interface SpeechToTextOptions {\n\t/** Language code */\n\tlanguage?: string;\n\t/** Model to use */\n\tmodel?: string;\n\t/** Enable continuous recognition */\n\tcontinuous?: boolean;\n\t/** Return interim results */\n\tinterimResults?: boolean;\n\t/** Maximum alternatives to return */\n\tmaxAlternatives?: number;\n}\n\n/**\n * Options for text-to-speech.\n */\nexport interface TextToSpeechOptions {\n\t/** Voice to use */\n\tvoice?: string;\n\t/** Model to use */\n\tmodel?: string;\n\t/** Speech speed */\n\tspeed?: number;\n\t/** Output format */\n\tformat?: \"mp3\" | \"wav\" | \"flac\" | \"aac\";\n\t/** Response format */\n\tresponse_format?: \"mp3\" | \"opus\" | \"aac\" | \"flac\";\n}\n\n// ============================================================================\n// Video Interfaces\n// ============================================================================\n\n/**\n * Video information.\n */\nexport interface VideoInfo {\n\t/** Video title */\n\ttitle?: string;\n\t/** Duration in seconds */\n\tduration?: number;\n\t/** Video URL */\n\turl: string;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Video description */\n\tdescription?: string;\n\t/** Uploader name */\n\tuploader?: string;\n\t/** View count */\n\tviewCount?: number;\n\t/** Upload date */\n\tuploadDate?: Date;\n\t/** Available formats */\n\tformats?: VideoFormat[];\n}\n\n/**\n * Video format information.\n */\nexport interface VideoFormat {\n\t/** Format ID */\n\tformatId: string;\n\t/** Download URL */\n\turl: string;\n\t/** File extension */\n\textension: string;\n\t/** Quality label */\n\tquality: string;\n\t/** File size in bytes */\n\tfileSize?: number;\n\t/** Video codec */\n\tvideoCodec?: string;\n\t/** Audio codec */\n\taudioCodec?: string;\n\t/** Resolution (e.g., \"1920x1080\") */\n\tresolution?: string;\n\t/** Frames per second */\n\tfps?: number;\n\t/** Bitrate */\n\tbitrate?: number;\n}\n\n/**\n * Video download options.\n */\nexport interface VideoDownloadOptions {\n\t/** Preferred format */\n\tformat?: string;\n\t/** Quality preference */\n\tquality?: \"best\" | \"worst\" | \"bestvideo\" | \"bestaudio\" | string;\n\t/** Output file path */\n\toutputPath?: string;\n\t/** Extract audio only */\n\taudioOnly?: boolean;\n\t/** Extract video only (no audio) */\n\tvideoOnly?: boolean;\n\t/** Download subtitles */\n\tsubtitles?: boolean;\n\t/** Embed subtitles in video */\n\tembedSubs?: boolean;\n\t/** Write info JSON file */\n\twriteInfoJson?: boolean;\n}\n\n/**\n * Video processing options.\n */\nexport interface VideoProcessingOptions {\n\t/** Start time in seconds */\n\tstartTime?: number;\n\t/** End time in seconds */\n\tendTime?: number;\n\t/** Output format */\n\toutputFormat?: string;\n\t/** Target resolution */\n\tresolution?: string;\n\t/** Target bitrate */\n\tbitrate?: string;\n\t/** Target framerate */\n\tframerate?: number;\n\t/** Audio codec */\n\taudioCodec?: string;\n\t/** Video codec */\n\tvideoCodec?: string;\n}\n\n// ============================================================================\n// Browser Interfaces\n// ============================================================================\n\n/**\n * Browser navigation options.\n */\nexport interface BrowserNavigationOptions {\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Wait until condition */\n\twaitUntil?: \"load\" | \"domcontentloaded\" | \"networkidle0\" | \"networkidle2\";\n\t/** Viewport size */\n\tviewport?: {\n\t\twidth: number;\n\t\theight: number;\n\t};\n\t/** User agent string */\n\tuserAgent?: string;\n\t/** Additional headers */\n\theaders?: Record<string, string>;\n}\n\n/**\n * Screenshot options.\n */\nexport interface ScreenshotOptions {\n\t/** Capture full page */\n\tfullPage?: boolean;\n\t/** Clip region */\n\tclip?: {\n\t\tx: number;\n\t\ty: number;\n\t\twidth: number;\n\t\theight: number;\n\t};\n\t/** Image format */\n\tformat?: \"png\" | \"jpeg\" | \"webp\";\n\t/** Image quality (0-100) */\n\tquality?: number;\n\t/** Omit background */\n\tomitBackground?: boolean;\n}\n\n/**\n * Element selector options.\n */\nexport interface ElementSelector {\n\t/** CSS selector */\n\tselector: string;\n\t/** Text content to match */\n\ttext?: string;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * Extracted content from a page.\n */\nexport interface ExtractedContent {\n\t/** Text content */\n\ttext: string;\n\t/** HTML content */\n\thtml: string;\n\t/** Links found on page */\n\tlinks: Array<{\n\t\turl: string;\n\t\ttext: string;\n\t}>;\n\t/** Images found on page */\n\timages: Array<{\n\t\tsrc: string;\n\t\talt?: string;\n\t}>;\n\t/** Page title */\n\ttitle?: string;\n\t/** Page metadata */\n\tmetadata?: Record<string, string>;\n}\n\n/**\n * Click options.\n */\nexport interface ClickOptions {\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Force click even if element is obscured */\n\tforce?: boolean;\n\t/** Wait for navigation after click */\n\twaitForNavigation?: boolean;\n}\n\n/**\n * Type/input options.\n */\nexport interface TypeOptions {\n\t/** Delay between keystrokes */\n\tdelay?: number;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Clear field before typing */\n\tclear?: boolean;\n}\n\n// ============================================================================\n// PDF Interfaces\n// ============================================================================\n\n/**\n * PDF text extraction result.\n */\nexport interface PdfExtractionResult {\n\t/** Extracted text */\n\ttext: string;\n\t/** Total page count */\n\tpageCount: number;\n\t/** PDF metadata */\n\tmetadata?: {\n\t\ttitle?: string;\n\t\tauthor?: string;\n\t\tcreatedAt?: Date;\n\t\tmodifiedAt?: Date;\n\t};\n}\n\n/**\n * PDF generation options.\n */\nexport interface PdfGenerationOptions {\n\t/** Paper format */\n\tformat?: \"A4\" | \"A3\" | \"Letter\";\n\t/** Page orientation */\n\torientation?: \"portrait\" | \"landscape\";\n\t/** Page margins */\n\tmargins?: {\n\t\ttop?: number;\n\t\tbottom?: number;\n\t\tleft?: number;\n\t\tright?: number;\n\t};\n\t/** Header content */\n\theader?: string;\n\t/** Footer content */\n\tfooter?: string;\n}\n\n/**\n * PDF conversion options.\n */\nexport interface PdfConversionOptions {\n\t/** Output quality */\n\tquality?: \"high\" | \"medium\" | \"low\";\n\t/** Output format */\n\toutputFormat?: \"pdf\" | \"pdf/a\";\n\t/** Enable compression */\n\tcompression?: boolean;\n}\n\n// ============================================================================\n// Web Search Interfaces\n// ============================================================================\n\n/**\n * Web search options.\n */\nexport interface SearchOptions {\n\t/** Maximum results to return */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Language code */\n\tlanguage?: string;\n\t/** Region code */\n\tregion?: string;\n\t/** Date range filter */\n\tdateRange?: {\n\t\tstart?: Date;\n\t\tend?: Date;\n\t};\n\t/** File type filter */\n\tfileType?: string;\n\t/** Limit to specific site */\n\tsite?: string;\n\t/** Sort order */\n\tsortBy?: \"relevance\" | \"date\" | \"popularity\";\n\t/** Safe search level */\n\tsafeSearch?: \"strict\" | \"moderate\" | \"off\";\n}\n\n/**\n * A single search result.\n */\nexport interface SearchResult {\n\t/** Result title */\n\ttitle: string;\n\t/** Result URL */\n\turl: string;\n\t/** Result description/snippet */\n\tdescription: string;\n\t/** Display URL */\n\tdisplayUrl?: string;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Published date */\n\tpublishedDate?: Date;\n\t/** Source name */\n\tsource?: string;\n\t/** Relevance score */\n\trelevanceScore?: number;\n\t/** Text snippet */\n\tsnippet?: string;\n}\n\n/**\n * Search response containing results.\n */\nexport interface SearchResponse {\n\t/** Original query */\n\tquery: string;\n\t/** Search results */\n\tresults: SearchResult[];\n\t/** Total available results */\n\ttotalResults?: number;\n\t/** Search time in seconds */\n\tsearchTime?: number;\n\t/** Query suggestions */\n\tsuggestions?: string[];\n\t/** Token for next page */\n\tnextPageToken?: string;\n\t/** Related search queries */\n\trelatedSearches?: string[];\n}\n\n/**\n * News search options.\n */\nexport interface NewsSearchOptions extends SearchOptions {\n\t/** News category */\n\tcategory?:\n\t\t| \"general\"\n\t\t| \"business\"\n\t\t| \"entertainment\"\n\t\t| \"health\"\n\t\t| \"science\"\n\t\t| \"sports\"\n\t\t| \"technology\";\n\t/** Freshness filter */\n\tfreshness?: \"day\" | \"week\" | \"month\";\n}\n\n/**\n * Image search options.\n */\nexport interface ImageSearchOptions extends SearchOptions {\n\t/** Image size filter */\n\tsize?: \"small\" | \"medium\" | \"large\" | \"wallpaper\" | \"any\";\n\t/** Color filter */\n\tcolor?:\n\t\t| \"color\"\n\t\t| \"monochrome\"\n\t\t| \"red\"\n\t\t| \"orange\"\n\t\t| \"yellow\"\n\t\t| \"green\"\n\t\t| \"blue\"\n\t\t| \"purple\"\n\t\t| \"pink\"\n\t\t| \"brown\"\n\t\t| \"black\"\n\t\t| \"gray\"\n\t\t| \"white\";\n\t/** Image type filter */\n\ttype?: \"photo\" | \"clipart\" | \"line\" | \"animated\";\n\t/** Image layout filter */\n\tlayout?: \"square\" | \"wide\" | \"tall\" | \"any\";\n\t/** License filter */\n\tlicense?: \"any\" | \"public\" | \"share\" | \"sharecommercially\" | \"modify\";\n}\n\n/**\n * Video search options.\n */\nexport interface VideoSearchOptions extends SearchOptions {\n\t/** Duration filter */\n\tduration?: \"short\" | \"medium\" | \"long\" | \"any\";\n\t/** Resolution filter */\n\tresolution?: \"high\" | \"standard\" | \"any\";\n\t/** Quality filter */\n\tquality?: \"high\" | \"standard\" | \"any\";\n}\n\n// ============================================================================\n// Email Interfaces\n// ============================================================================\n\n/**\n * Email address with optional name.\n */\nexport interface EmailAddress {\n\t/** Email address */\n\temail: string;\n\t/** Display name */\n\tname?: string;\n}\n\n/**\n * Email attachment.\n */\nexport interface EmailAttachment {\n\t/** Filename */\n\tfilename: string;\n\t/** Content as buffer or base64 string */\n\tcontent: Buffer | string;\n\t/** MIME type */\n\tcontentType?: string;\n\t/** Content disposition */\n\tcontentDisposition?: \"attachment\" | \"inline\";\n\t/** Content ID for inline attachments */\n\tcid?: string;\n}\n\n/**\n * Email message.\n */\nexport interface EmailMessage {\n\t/** Sender address */\n\tfrom: EmailAddress;\n\t/** Recipients */\n\tto: EmailAddress[];\n\t/** CC recipients */\n\tcc?: EmailAddress[];\n\t/** BCC recipients */\n\tbcc?: EmailAddress[];\n\t/** Email subject */\n\tsubject: string;\n\t/** Plain text body */\n\ttext?: string;\n\t/** HTML body */\n\thtml?: string;\n\t/** Attachments */\n\tattachments?: EmailAttachment[];\n\t/** Reply-to address */\n\treplyTo?: EmailAddress;\n\t/** Send date */\n\tdate?: Date;\n\t/** Message ID */\n\tmessageId?: string;\n\t/** References header */\n\treferences?: string[];\n\t/** In-Reply-To header */\n\tinReplyTo?: string;\n\t/** Priority level */\n\tpriority?: \"high\" | \"normal\" | \"low\";\n}\n\n/**\n * Email send options.\n */\nexport interface EmailSendOptions {\n\t/** Number of retries */\n\tretry?: number;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Track email opens */\n\ttrackOpens?: boolean;\n\t/** Track link clicks */\n\ttrackClicks?: boolean;\n\t/** Tags for categorization */\n\ttags?: string[];\n}\n\n/**\n * Email search options.\n */\nexport interface EmailSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by sender */\n\tfrom?: string;\n\t/** Filter by recipient */\n\tto?: string;\n\t/** Filter by subject */\n\tsubject?: string;\n\t/** Filter by folder */\n\tfolder?: string;\n\t/** Filter emails since date */\n\tsince?: Date;\n\t/** Filter emails before date */\n\tbefore?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter unread only */\n\tunread?: boolean;\n\t/** Filter flagged only */\n\tflagged?: boolean;\n\t/** Filter with attachments only */\n\thasAttachments?: boolean;\n}\n\n/**\n * Email folder.\n */\nexport interface EmailFolder {\n\t/** Folder name */\n\tname: string;\n\t/** Folder path */\n\tpath: string;\n\t/** Folder type */\n\ttype: \"inbox\" | \"sent\" | \"drafts\" | \"trash\" | \"spam\" | \"custom\";\n\t/** Total message count */\n\tmessageCount?: number;\n\t/** Unread message count */\n\tunreadCount?: number;\n\t/** Child folders */\n\tchildren?: EmailFolder[];\n}\n\n/**\n * Email account information.\n */\nexport interface EmailAccount {\n\t/** Email address */\n\temail: string;\n\t/** Display name */\n\tname?: string;\n\t/** Email provider */\n\tprovider?: string;\n\t/** Available folders */\n\tfolders?: EmailFolder[];\n\t/** Storage used in bytes */\n\tquotaUsed?: number;\n\t/** Storage limit in bytes */\n\tquotaLimit?: number;\n}\n\n// ============================================================================\n// Message Interfaces\n// ============================================================================\n\n/**\n * Message participant information.\n */\nexport interface MessageParticipant {\n\t/** Participant ID */\n\tid: UUID;\n\t/** Display name */\n\tname: string;\n\t/** Username */\n\tusername?: string;\n\t/** Avatar URL */\n\tavatar?: string;\n\t/** Online status */\n\tstatus?: \"online\" | \"offline\" | \"away\" | \"busy\";\n}\n\n/**\n * Message attachment.\n */\nexport interface MessageAttachment {\n\t/** Attachment ID */\n\tid: UUID;\n\t/** Filename */\n\tfilename: string;\n\t/** File URL */\n\turl: string;\n\t/** MIME type */\n\tmimeType: string;\n\t/** File size in bytes */\n\tsize: number;\n\t/** Width for images/videos */\n\twidth?: number;\n\t/** Height for images/videos */\n\theight?: number;\n\t/** Duration for audio/video */\n\tduration?: number;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n}\n\n/**\n * Message reaction.\n */\nexport interface MessageReaction {\n\t/** Emoji used */\n\temoji: string;\n\t/** Number of reactions */\n\tcount: number;\n\t/** User IDs who reacted */\n\tusers: UUID[];\n\t/** Whether current user has reacted */\n\thasReacted: boolean;\n}\n\n/**\n * Message reference (reply/forward/quote).\n */\nexport interface MessageReference {\n\t/** Referenced message ID */\n\tmessageId: UUID;\n\t/** Channel of referenced message */\n\tchannelId: UUID;\n\t/** Type of reference */\n\ttype: \"reply\" | \"forward\" | \"quote\";\n}\n\n/**\n * Message content.\n */\nexport interface MessageContent {\n\t/** Plain text content */\n\ttext?: string;\n\t/** HTML content */\n\thtml?: string;\n\t/** Markdown content */\n\tmarkdown?: string;\n\t/** Attachments */\n\tattachments?: MessageAttachment[];\n\t/** Reactions */\n\treactions?: MessageReaction[];\n\t/** Reference to another message */\n\treference?: MessageReference;\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Embedded content */\n\tembeds?: Array<{\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\turl?: string;\n\t\timage?: string;\n\t\tfields?: Array<{\n\t\t\tname: string;\n\t\t\tvalue: string;\n\t\t\tinline?: boolean;\n\t\t}>;\n\t}>;\n}\n\n/**\n * Message information.\n */\nexport interface MessageInfo {\n\t/** Message ID */\n\tid: UUID;\n\t/** Channel ID */\n\tchannelId: UUID;\n\t/** Sender ID */\n\tsenderId: UUID;\n\t/** Message content */\n\tcontent: MessageContent;\n\t/** Sent timestamp */\n\ttimestamp: Date;\n\t/** Edit timestamp */\n\tedited?: Date;\n\t/** Deletion timestamp */\n\tdeleted?: Date;\n\t/** Whether message is pinned */\n\tpinned?: boolean;\n\t/** Thread information */\n\tthread?: {\n\t\tid: UUID;\n\t\tmessageCount: number;\n\t\tparticipants: UUID[];\n\t\tlastMessageAt: Date;\n\t};\n}\n\n/**\n * Message send options.\n */\nexport interface MessageSendOptions {\n\t/** Reply to message ID */\n\treplyTo?: UUID;\n\t/** Ephemeral (only visible to sender) */\n\tephemeral?: boolean;\n\t/** Silent (no notification) */\n\tsilent?: boolean;\n\t/** Scheduled send time */\n\tscheduled?: Date;\n\t/** Thread ID */\n\tthread?: UUID;\n\t/** Nonce for deduplication */\n\tnonce?: string;\n}\n\n/**\n * Message search options.\n */\nexport interface MessageSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by channel */\n\tchannelId?: UUID;\n\t/** Filter by sender */\n\tsenderId?: UUID;\n\t/** Filter messages before date */\n\tbefore?: Date;\n\t/** Filter messages after date */\n\tafter?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter with attachments only */\n\thasAttachments?: boolean;\n\t/** Filter pinned only */\n\tpinned?: boolean;\n\t/** Filter mentioning user */\n\tmentions?: UUID;\n}\n\n/**\n * Message channel.\n */\nexport interface MessageChannel {\n\t/** Channel ID */\n\tid: UUID;\n\t/** Channel name */\n\tname: string;\n\t/** Channel type */\n\ttype: \"text\" | \"voice\" | \"dm\" | \"group\" | \"announcement\" | \"thread\";\n\t/** Channel description */\n\tdescription?: string;\n\t/** Channel participants */\n\tparticipants?: MessageParticipant[];\n\t/** User permissions */\n\tpermissions?: {\n\t\tcanSend: boolean;\n\t\tcanRead: boolean;\n\t\tcanDelete: boolean;\n\t\tcanPin: boolean;\n\t\tcanManage: boolean;\n\t};\n\t/** Last message timestamp */\n\tlastMessageAt?: Date;\n\t/** Total message count */\n\tmessageCount?: number;\n\t/** Unread message count */\n\tunreadCount?: number;\n}\n\n// ============================================================================\n// Post/Social Media Interfaces\n// ============================================================================\n\n/**\n * Post media content.\n */\nexport interface PostMedia {\n\t/** Media ID */\n\tid: UUID;\n\t/** Media URL */\n\turl: string;\n\t/** Media type */\n\ttype: \"image\" | \"video\" | \"audio\" | \"document\";\n\t/** MIME type */\n\tmimeType: string;\n\t/** File size in bytes */\n\tsize: number;\n\t/** Width for images/videos */\n\twidth?: number;\n\t/** Height for images/videos */\n\theight?: number;\n\t/** Duration for audio/video */\n\tduration?: number;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Description */\n\tdescription?: string;\n\t/** Alt text for accessibility */\n\taltText?: string;\n}\n\n/**\n * Post location.\n */\nexport interface PostLocation {\n\t/** Location name */\n\tname: string;\n\t/** Address */\n\taddress?: string;\n\t/** Coordinates */\n\tcoordinates?: {\n\t\tlatitude: number;\n\t\tlongitude: number;\n\t};\n\t/** Place ID from location service */\n\tplaceId?: string;\n}\n\n/**\n * Post author information.\n */\nexport interface PostAuthor {\n\t/** Author ID */\n\tid: UUID;\n\t/** Username */\n\tusername: string;\n\t/** Display name */\n\tdisplayName: string;\n\t/** Avatar URL */\n\tavatar?: string;\n\t/** Verified badge */\n\tverified?: boolean;\n\t/** Follower count */\n\tfollowerCount?: number;\n\t/** Following count */\n\tfollowingCount?: number;\n\t/** Bio */\n\tbio?: string;\n\t/** Website URL */\n\twebsite?: string;\n}\n\n/**\n * Post engagement metrics.\n */\nexport interface PostEngagement {\n\t/** Number of likes */\n\tlikes: number;\n\t/** Number of shares */\n\tshares: number;\n\t/** Number of comments */\n\tcomments: number;\n\t/** Number of views */\n\tviews?: number;\n\t/** Whether current user has liked */\n\thasLiked: boolean;\n\t/** Whether current user has shared */\n\thasShared: boolean;\n\t/** Whether current user has commented */\n\thasCommented: boolean;\n\t/** Whether current user has saved */\n\thasSaved: boolean;\n}\n\n/**\n * Post content.\n */\nexport interface PostContent {\n\t/** Text content */\n\ttext?: string;\n\t/** HTML content */\n\thtml?: string;\n\t/** Media attachments */\n\tmedia?: PostMedia[];\n\t/** Location */\n\tlocation?: PostLocation;\n\t/** Hashtags */\n\ttags?: string[];\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Link previews */\n\tlinks?: Array<{\n\t\turl: string;\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\timage?: string;\n\t}>;\n\t/** Poll */\n\tpoll?: {\n\t\tquestion: string;\n\t\toptions: Array<{\n\t\t\ttext: string;\n\t\t\tvotes: number;\n\t\t}>;\n\t\texpiresAt?: Date;\n\t\tmultipleChoice?: boolean;\n\t};\n}\n\n/**\n * Post information.\n */\nexport interface PostInfo {\n\t/** Post ID */\n\tid: UUID;\n\t/** Post author */\n\tauthor: PostAuthor;\n\t/** Post content */\n\tcontent: PostContent;\n\t/** Platform name */\n\tplatform: string;\n\t/** Platform-specific ID */\n\tplatformId: string;\n\t/** Post URL */\n\turl: string;\n\t/** Created timestamp */\n\tcreatedAt: Date;\n\t/** Edited timestamp */\n\teditedAt?: Date;\n\t/** Scheduled timestamp */\n\tscheduledAt?: Date;\n\t/** Engagement metrics */\n\tengagement: PostEngagement;\n\t/** Visibility level */\n\tvisibility: \"public\" | \"private\" | \"followers\" | \"friends\" | \"unlisted\";\n\t/** Reply to post ID */\n\treplyTo?: UUID;\n\t/** Thread information */\n\tthread?: {\n\t\tid: UUID;\n\t\tposition: number;\n\t\ttotal: number;\n\t};\n\t/** Cross-post information */\n\tcrossPosted?: Array<{\n\t\tplatform: string;\n\t\tplatformId: string;\n\t\turl: string;\n\t}>;\n}\n\n/**\n * Post creation options.\n */\nexport interface PostCreateOptions {\n\t/** Target platforms */\n\tplatforms?: string[];\n\t/** Scheduled time */\n\tscheduledAt?: Date;\n\t/** Visibility level */\n\tvisibility?: PostInfo[\"visibility\"];\n\t/** Reply to post ID */\n\treplyTo?: UUID;\n\t/** Create as thread */\n\tthread?: boolean;\n\t/** Location */\n\tlocation?: PostLocation;\n\t/** Hashtags */\n\ttags?: string[];\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Enable comments */\n\tenableComments?: boolean;\n\t/** Enable sharing */\n\tenableSharing?: boolean;\n\t/** Content warning */\n\tcontentWarning?: string;\n\t/** Mark as sensitive */\n\tsensitive?: boolean;\n}\n\n/**\n * Post search options.\n */\nexport interface PostSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by author */\n\tauthor?: UUID;\n\t/** Filter by platform */\n\tplatform?: string;\n\t/** Filter by tags */\n\ttags?: string[];\n\t/** Filter by mentions */\n\tmentions?: UUID[];\n\t/** Filter posts since date */\n\tsince?: Date;\n\t/** Filter posts before date */\n\tbefore?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter with media only */\n\thasMedia?: boolean;\n\t/** Filter with location only */\n\thasLocation?: boolean;\n\t/** Filter by visibility */\n\tvisibility?: PostInfo[\"visibility\"];\n\t/** Sort order */\n\tsortBy?: \"date\" | \"engagement\" | \"relevance\";\n}\n\n/**\n * Post analytics.\n */\nexport interface PostAnalytics {\n\t/** Post ID */\n\tpostId: UUID;\n\t/** Platform name */\n\tplatform: string;\n\t/** Total impressions */\n\timpressions: number;\n\t/** Unique reach */\n\treach: number;\n\t/** Engagement metrics */\n\tengagement: PostEngagement;\n\t/** Link clicks */\n\tclicks: number;\n\t/** Shares */\n\tshares: number;\n\t/** Saves */\n\tsaves: number;\n\t/** Demographics */\n\tdemographics?: {\n\t\tage?: Record<string, number>;\n\t\tgender?: Record<string, number>;\n\t\tlocation?: Record<string, number>;\n\t};\n\t/** Top performing hours */\n\ttopPerformingHours?: Array<{\n\t\thour: number;\n\t\tengagement: number;\n\t}>;\n}\n",
|
|
459
459
|
"/**\n * Tool Policy Types\n *\n * Types and definitions for tool/action filtering and permissions in elizaOS.\n *\n * @module tools\n */\n\n// Re-export from channel-config to avoid duplication\nexport type { ToolPolicyConfig, ToolProfileId } from \"./channel-config\";\n\nimport type { ToolPolicyConfig, ToolProfileId } from \"./channel-config\";\n\n/**\n * Canonical tool name aliases for backward compatibility.\n * Maps legacy names to canonical names.\n */\nexport const TOOL_NAME_ALIASES: Record<string, string> = {\n\tbash: \"exec\",\n\t\"apply-patch\": \"apply_patch\",\n};\n\n/**\n * Predefined tool groups for easier policy configuration.\n * Use \"group:<name>\" syntax in policy configs (e.g., \"group:fs\").\n */\nexport const TOOL_GROUPS: Record<string, string[]> = {\n\t// Memory tools (provided by plugin-scratchpad)\n\t\"group:memory\": [\n\t\t\"scratchpad_search\",\n\t\t\"scratchpad_read\",\n\t\t\"read_attachment\",\n\t\t\"remove_from_scratchpad\",\n\t],\n\t// Web tools\n\t\"group:web\": [\"web_search\", \"web_fetch\"],\n\t// Basic workspace/file tools\n\t\"group:fs\": [\"read\", \"read_file\", \"write\", \"edit\", \"apply_patch\"],\n\t// Host/runtime execution tools\n\t\"group:runtime\": [\"exec\", \"process\"],\n\t// Session management tools\n\t\"group:sessions\": [\n\t\t\"sessions_list\",\n\t\t\"sessions_history\",\n\t\t\"sessions_send\",\n\t\t\"sessions_spawn\",\n\t\t\"session_status\",\n\t],\n\t// UI helpers\n\t\"group:ui\": [\"browser\", \"canvas\"],\n\t// Automation + infra\n\t\"group:automation\": [\"cron\", \"gateway\"],\n\t// Messaging surface\n\t\"group:messaging\": [\"message\"],\n\t// Nodes + device tools\n\t\"group:nodes\": [\"nodes\"],\n\t// All native tools (excludes provider plugins)\n\t\"group:all\": [\n\t\t\"browser\",\n\t\t\"canvas\",\n\t\t\"nodes\",\n\t\t\"cron\",\n\t\t\"message\",\n\t\t\"gateway\",\n\t\t\"agents_list\",\n\t\t\"sessions_list\",\n\t\t\"sessions_history\",\n\t\t\"sessions_send\",\n\t\t\"sessions_spawn\",\n\t\t\"session_status\",\n\t\t\"scratchpad_search\",\n\t\t\"scratchpad_read\",\n\t\t\"read_attachment\",\n\t\t\"read_file\",\n\t\t\"remove_from_scratchpad\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"image\",\n\t\t\"read\",\n\t\t\"write\",\n\t\t\"edit\",\n\t\t\"apply_patch\",\n\t\t\"exec\",\n\t\t\"process\",\n\t],\n};\n\n/**\n * Predefined tool profiles with default allow/deny policies.\n */\nexport const TOOL_PROFILES: Record<ToolProfileId, ToolPolicyConfig> = {\n\tminimal: {\n\t\tallow: [\"session_status\"],\n\t},\n\tcoding: {\n\t\tallow: [\n\t\t\t\"group:fs\",\n\t\t\t\"group:runtime\",\n\t\t\t\"group:sessions\",\n\t\t\t\"group:memory\",\n\t\t\t\"image\",\n\t\t],\n\t},\n\tmessaging: {\n\t\tallow: [\n\t\t\t\"group:messaging\",\n\t\t\t\"sessions_list\",\n\t\t\t\"sessions_history\",\n\t\t\t\"sessions_send\",\n\t\t\t\"session_status\",\n\t\t],\n\t},\n\tfull: {\n\t\t// No restrictions - all tools allowed\n\t},\n};\n\n/**\n * Plugin tool groups for dynamic tool resolution.\n */\nexport interface PluginToolGroups {\n\t/** All tool names from plugins */\n\tall: string[];\n\t/** Tool names organized by plugin ID */\n\tbyPlugin: Map<string, string[]>;\n}\n\n/**\n * Result of allowlist resolution with diagnostics.\n */\nexport interface AllowlistResolution {\n\t/** The resolved policy after processing */\n\tpolicy: ToolPolicyConfig | undefined;\n\t/** Entries in the allowlist that weren't recognized */\n\tunknownAllowlist: string[];\n\t/** Whether the allowlist was stripped (contained only plugin tools) */\n\tstrippedAllowlist: boolean;\n}\n\n/**\n * Tool policy evaluation options.\n */\nexport interface ToolPolicyEvaluationOptions {\n\t/** The character's tool profile */\n\tprofile?: ToolProfileId;\n\t/** Character-level tool policy overrides */\n\tcharacterPolicy?: ToolPolicyConfig;\n\t/** Channel-specific tool policy overrides */\n\tchannelPolicy?: ToolPolicyConfig;\n\t/** Provider-specific tool policy overrides */\n\tproviderPolicy?: ToolPolicyConfig;\n\t/** Plugin tool groups for resolution */\n\tpluginGroups?: PluginToolGroups;\n\t/** Set of core tool names for validation */\n\tcoreTools?: Set<string>;\n}\n\n/**\n * Tool policy evaluation result.\n */\nexport interface ToolPolicyResult {\n\t/** Whether the tool is allowed */\n\tallowed: boolean;\n\t/** Reason for the decision */\n\treason: string;\n\t/** The effective policy after merging */\n\teffectivePolicy: ToolPolicyConfig;\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize a tool name to its canonical form.\n * Handles aliases and case normalization.\n *\n * @param name - The tool name to normalize\n * @returns The canonical tool name\n */\nexport function normalizeToolName(name: string): string {\n\tconst normalized = name.trim().toLowerCase();\n\treturn TOOL_NAME_ALIASES[normalized] ?? normalized;\n}\n\n/**\n * Normalize a list of tool names.\n *\n * @param list - The list of tool names to normalize\n * @returns Normalized list with empty entries filtered out\n */\nexport function normalizeToolList(list?: string[]): string[] {\n\tif (!list) {\n\t\treturn [];\n\t}\n\treturn list.map(normalizeToolName).filter(Boolean);\n}\n\n/**\n * Expand tool groups in a list to their constituent tools.\n * Handles both group references (e.g., \"group:fs\") and individual tools.\n *\n * @param list - The list containing tool names and/or group references\n * @returns Expanded list with all groups resolved to individual tools\n */\nexport function expandToolGroups(list?: string[]): string[] {\n\tconst normalized = normalizeToolList(list);\n\tconst expanded: string[] = [];\n\n\tfor (const value of normalized) {\n\t\tconst group = TOOL_GROUPS[value];\n\t\tif (group) {\n\t\t\texpanded.push(...group);\n\t\t\tcontinue;\n\t\t}\n\t\texpanded.push(value);\n\t}\n\n\treturn Array.from(new Set(expanded));\n}\n\n/**\n * Resolve a tool profile to its policy configuration.\n *\n * @param profile - The profile ID to resolve\n * @returns The policy configuration, or undefined if profile is invalid\n */\nexport function resolveToolProfilePolicy(\n\tprofile?: string,\n): ToolPolicyConfig | undefined {\n\tif (!profile) {\n\t\treturn undefined;\n\t}\n\tconst resolved = TOOL_PROFILES[profile as ToolProfileId];\n\tif (!resolved) {\n\t\treturn undefined;\n\t}\n\t// Return undefined for 'full' profile (no restrictions)\n\tif (!resolved.allow && !resolved.deny) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tallow: resolved.allow ? [...resolved.allow] : undefined,\n\t\tdeny: resolved.deny ? [...resolved.deny] : undefined,\n\t};\n}\n\n/**\n * Collect all explicit allow entries from multiple policies.\n *\n * @param policies - Array of policies to collect from\n * @returns Combined allowlist entries\n */\nexport function collectExplicitAllowlist(\n\tpolicies: Array<ToolPolicyConfig | undefined>,\n): string[] {\n\tconst entries: string[] = [];\n\n\tfor (const policy of policies) {\n\t\tif (!policy?.allow) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const value of policy.allow) {\n\t\t\tif (typeof value !== \"string\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst trimmed = value.trim();\n\t\t\tif (trimmed) {\n\t\t\t\tentries.push(trimmed);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Build plugin tool groups from a list of tools with metadata.\n *\n * @param params - Tools and metadata accessor\n * @returns Plugin tool groups organized by plugin ID\n */\nexport function buildPluginToolGroups<T extends { name: string }>(params: {\n\ttools: T[];\n\ttoolMeta: (tool: T) => { pluginId: string } | undefined;\n}): PluginToolGroups {\n\tconst all: string[] = [];\n\tconst byPlugin = new Map<string, string[]>();\n\n\tfor (const tool of params.tools) {\n\t\tconst meta = params.toolMeta(tool);\n\t\tif (!meta) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst name = normalizeToolName(tool.name);\n\t\tall.push(name);\n\t\tconst pluginId = meta.pluginId.toLowerCase();\n\t\tconst list = byPlugin.get(pluginId) ?? [];\n\t\tlist.push(name);\n\t\tbyPlugin.set(pluginId, list);\n\t}\n\n\treturn { all, byPlugin };\n}\n\n/**\n * Expand plugin group references in a list.\n *\n * @param list - The list containing potential plugin group references\n * @param groups - Plugin tool groups for resolution\n * @returns Expanded list with plugin groups resolved\n */\nexport function expandPluginGroups(\n\tlist: string[] | undefined,\n\tgroups: PluginToolGroups,\n): string[] | undefined {\n\tif (!list || list.length === 0) {\n\t\treturn list;\n\t}\n\n\tconst expanded: string[] = [];\n\tfor (const entry of list) {\n\t\tconst normalized = normalizeToolName(entry);\n\t\tif (normalized === \"group:plugins\") {\n\t\t\tif (groups.all.length > 0) {\n\t\t\t\texpanded.push(...groups.all);\n\t\t\t} else {\n\t\t\t\texpanded.push(normalized);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tconst tools = groups.byPlugin.get(normalized);\n\t\tif (tools && tools.length > 0) {\n\t\t\texpanded.push(...tools);\n\t\t\tcontinue;\n\t\t}\n\t\texpanded.push(normalized);\n\t}\n\n\treturn Array.from(new Set(expanded));\n}\n\n/**\n * Expand a policy with plugin group resolution.\n *\n * @param policy - The policy to expand\n * @param groups - Plugin tool groups for resolution\n * @returns Policy with plugin groups expanded\n */\nexport function expandPolicyWithPluginGroups(\n\tpolicy: ToolPolicyConfig | undefined,\n\tgroups: PluginToolGroups,\n): ToolPolicyConfig | undefined {\n\tif (!policy) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tallow: expandPluginGroups(policy.allow, groups),\n\t\tdeny: expandPluginGroups(policy.deny, groups),\n\t};\n}\n\n/**\n * Strip plugin-only allowlist to prevent accidentally disabling core tools.\n * When an allowlist contains only plugin tools, we remove it to avoid\n * inadvertently blocking core functionality.\n *\n * @param policy - The policy to check\n * @param groups - Plugin tool groups\n * @param coreTools - Set of core tool names\n * @returns Resolution result with diagnostic information\n */\nexport function stripPluginOnlyAllowlist(\n\tpolicy: ToolPolicyConfig | undefined,\n\tgroups: PluginToolGroups,\n\tcoreTools: Set<string>,\n): AllowlistResolution {\n\tif (!policy?.allow || policy.allow.length === 0) {\n\t\treturn { policy, unknownAllowlist: [], strippedAllowlist: false };\n\t}\n\n\tconst normalized = normalizeToolList(policy.allow);\n\tif (normalized.length === 0) {\n\t\treturn { policy, unknownAllowlist: [], strippedAllowlist: false };\n\t}\n\n\tconst pluginIds = new Set(groups.byPlugin.keys());\n\tconst pluginTools = new Set(groups.all);\n\tconst unknownAllowlist: string[] = [];\n\tlet hasCoreEntry = false;\n\n\tfor (const entry of normalized) {\n\t\tif (entry === \"*\") {\n\t\t\thasCoreEntry = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconst isPluginEntry =\n\t\t\tentry === \"group:plugins\" ||\n\t\t\tpluginIds.has(entry) ||\n\t\t\tpluginTools.has(entry);\n\t\tconst expanded = expandToolGroups([entry]);\n\t\tconst isCoreEntry = expanded.some((tool) => coreTools.has(tool));\n\t\tif (isCoreEntry) {\n\t\t\thasCoreEntry = true;\n\t\t}\n\t\tif (!isCoreEntry && !isPluginEntry) {\n\t\t\tunknownAllowlist.push(entry);\n\t\t}\n\t}\n\n\tconst strippedAllowlist = !hasCoreEntry;\n\n\treturn {\n\t\tpolicy: strippedAllowlist ? { ...policy, allow: undefined } : policy,\n\t\tunknownAllowlist: Array.from(new Set(unknownAllowlist)),\n\t\tstrippedAllowlist,\n\t};\n}\n\n/**\n * Merge multiple tool policies into a single effective policy.\n * Later policies take precedence for conflicts.\n *\n * @param policies - Policies to merge in order of precedence\n * @returns Merged policy\n */\nexport function mergeToolPolicies(\n\t...policies: Array<ToolPolicyConfig | undefined>\n): ToolPolicyConfig {\n\tconst result: ToolPolicyConfig = {};\n\n\tfor (const policy of policies) {\n\t\tif (!policy) continue;\n\n\t\tif (policy.allow !== undefined) {\n\t\t\t// If a more specific policy has an allow list, it replaces (not merges)\n\t\t\tresult.allow = [...(policy.allow || [])];\n\t\t}\n\n\t\tif (policy.deny !== undefined) {\n\t\t\t// Deny lists are additive - combine them\n\t\t\tresult.deny = [...(result.deny || []), ...(policy.deny || [])];\n\t\t}\n\t}\n\n\t// Deduplicate\n\tif (result.allow) {\n\t\tresult.allow = Array.from(new Set(result.allow));\n\t}\n\tif (result.deny) {\n\t\tresult.deny = Array.from(new Set(result.deny));\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if a tool is allowed by a policy.\n *\n * @param toolName - The tool name to check\n * @param policy - The policy to evaluate against\n * @returns Whether the tool is allowed\n */\nexport function isToolAllowedByPolicy(\n\ttoolName: string,\n\tpolicy: ToolPolicyConfig | undefined,\n): boolean {\n\tconst normalizedName = normalizeToolName(toolName);\n\n\t// No policy means all tools allowed\n\tif (!policy) {\n\t\treturn true;\n\t}\n\n\t// Check deny list first (deny takes precedence)\n\tif (policy.deny && policy.deny.length > 0) {\n\t\tconst expandedDeny = expandToolGroups(policy.deny);\n\t\tif (expandedDeny.includes(normalizedName)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Check allow list\n\tif (policy.allow && policy.allow.length > 0) {\n\t\tconst expandedAllow = expandToolGroups(policy.allow);\n\t\t// Wildcard allows everything not denied\n\t\tif (expandedAllow.includes(\"*\")) {\n\t\t\treturn true;\n\t\t}\n\t\treturn expandedAllow.includes(normalizedName);\n\t}\n\n\t// No allow list means all tools allowed (if not denied)\n\treturn true;\n}\n",
|
|
460
|
-
"import type { UUID } from \"./primitives\";\n\nexport const TRIGGER_SCHEMA_VERSION = 1 as const;\n\nexport type TriggerType = \"interval\" | \"once\" | \"cron\";\nexport type TriggerWakeMode = \"inject_now\" | \"next_autonomy_cycle\";\nexport type TriggerLastStatus = \"success\" | \"error\" | \"skipped\";\nexport type TriggerKind = \"text\" | \"workflow\";\n\nexport interface TriggerConfig {\n\tversion: typeof TRIGGER_SCHEMA_VERSION;\n\ttriggerId: UUID;\n\tdisplayName: string;\n\tinstructions: string;\n\ttriggerType: TriggerType;\n\tenabled: boolean;\n\twakeMode: TriggerWakeMode;\n\tcreatedBy: string;\n\ttimezone?: string;\n\tintervalMs?: number;\n\tscheduledAtIso?: string;\n\tcronExpression?: string;\n\tmaxRuns?: number;\n\trunCount: number;\n\tnextRunAtMs?: number;\n\tlastRunAtIso?: string;\n\tlastStatus?: TriggerLastStatus;\n\tlastError?: string;\n\tdedupeKey?: string;\n\t// When undefined, treat as \"text\" for back-compat.\n\tkind?: TriggerKind;\n\tworkflowId?: string;\n\tworkflowName?: string;\n}\n\nexport interface TriggerRunRecord {\n\ttriggerRunId: UUID;\n\ttriggerId: UUID;\n\ttaskId: UUID;\n\tstartedAt: number;\n\tfinishedAt: number;\n\tstatus: TriggerLastStatus;\n\terror?: string;\n\tlatencyMs: number;\n\tsource: \"scheduler\" | \"manual\";\n}\n",
|
|
460
|
+
"import type { UUID } from \"./primitives\";\n\nexport const TRIGGER_SCHEMA_VERSION = 1 as const;\n\nexport type TriggerType = \"interval\" | \"once\" | \"cron\" | \"event\";\nexport type TriggerWakeMode = \"inject_now\" | \"next_autonomy_cycle\";\nexport type TriggerLastStatus = \"success\" | \"error\" | \"skipped\";\nexport type TriggerKind = \"text\" | \"workflow\";\n\nexport interface TriggerConfig {\n\tversion: typeof TRIGGER_SCHEMA_VERSION;\n\ttriggerId: UUID;\n\tdisplayName: string;\n\tinstructions: string;\n\ttriggerType: TriggerType;\n\tenabled: boolean;\n\twakeMode: TriggerWakeMode;\n\tcreatedBy: string;\n\ttimezone?: string;\n\tintervalMs?: number;\n\tscheduledAtIso?: string;\n\tcronExpression?: string;\n\teventKind?: string;\n\tmaxRuns?: number;\n\trunCount: number;\n\tnextRunAtMs?: number;\n\tlastRunAtIso?: string;\n\tlastStatus?: TriggerLastStatus;\n\tlastError?: string;\n\tdedupeKey?: string;\n\t// When undefined, treat as \"text\" for back-compat.\n\tkind?: TriggerKind;\n\tworkflowId?: string;\n\tworkflowName?: string;\n}\n\nexport interface TriggerRunRecord {\n\ttriggerRunId: UUID;\n\ttriggerId: UUID;\n\ttaskId: UUID;\n\tstartedAt: number;\n\tfinishedAt: number;\n\tstatus: TriggerLastStatus;\n\terror?: string;\n\tlatencyMs: number;\n\tsource: \"scheduler\" | \"manual\" | \"event\";\n\teventKind?: string;\n}\n",
|
|
461
461
|
"// Core types\n\nexport { logger } from \"../logger\";\n// Utilities that are part of the public API.\nexport { addHeader, composePromptFromState, parseKeyValueXml } from \"../utils\";\nexport * from \"./agent\";\n// Channel configuration types for plugins\nexport * from \"./channel-config\";\nexport * from \"./components\";\nexport * from \"./database\";\nexport * from \"./environment\";\nexport * from \"./events\";\nexport * from \"./hook\";\nexport * from \"./knowledge\";\nexport * from \"./memory\";\nexport * from \"./memory-storage\";\nexport * from \"./messaging\";\nexport * from \"./model\";\n// Onboarding types\nexport * from \"./onboarding\";\nexport * from \"./pairing\";\nexport * from \"./payment\";\nexport * from \"./pipeline-hooks\";\nexport * from \"./plugin\";\nexport * from \"./plugin-store\";\nexport * from \"./primitives\";\nexport * from \"./prompt-batcher\";\nexport * from \"./prompt-optimization-hooks\";\nexport * from \"./prompt-optimization-score-card\";\nexport * from \"./prompt-optimization-trace\";\nexport * from \"./prompts\";\n// Proto-generated types (single source of truth)\n// These types are generated from /schemas/eliza/v1/*.proto\n// Use these for new code and cross-language interoperability\nexport * as proto from \"./proto.js\";\n// Re-export proto utilities for JSON conversion\n// JsonValue is also exported from primitives.ts, but we explicitly export it here for clarity\nexport { fromJson, type JsonObject, type JsonValue, toJson } from \"./proto.js\";\nexport * from \"./runtime\";\nexport * from \"./schema\";\nexport * from \"./schema-builder\";\nexport * from \"./service\";\nexport * from \"./service-interfaces\";\nexport * from \"./settings\";\nexport * from \"./state\";\nexport * from \"./streaming\";\nexport * from \"./task\";\nexport * from \"./tee\";\nexport * from \"./testing\";\nexport * from \"./tools\";\nexport * from \"./trigger\";\n",
|
|
462
462
|
"import { logger } from \"./logger\";\nimport {\n\ttype Entity,\n\ttype IAgentRuntime,\n\ttype Memory,\n\tModelType,\n\ttype Relationship,\n\ttype State,\n\ttype UUID,\n\ttype World,\n} from \"./types\";\nimport * as utils from \"./utils\";\nimport { stableStringify } from \"./utils/deterministic\";\n\ntype EntityDetailsRecord = Pick<Entity, \"id\" | \"names\"> & {\n\tname?: string;\n\tdata: string;\n};\n\nconst ENTITY_DETAILS_CACHE_TTL_MS = 1_000;\nconst entityDetailsCache = new WeakMap<\n\tIAgentRuntime,\n\tMap<string, { expiresAt: number; promise: Promise<EntityDetailsRecord[]> }>\n>();\n\ninterface EntityMatch {\n\tname?: string;\n\treason?: string;\n}\n\ninterface ParsedResolution {\n\tresolvedId?: string;\n\tconfidence?: string;\n\tmatches?: {\n\t\tmatch?: EntityMatch | EntityMatch[];\n\t};\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction normalizeEntityMatch(value: unknown): EntityMatch | null {\n\tif (!isRecord(value)) return null;\n\n\tconst name = typeof value.name === \"string\" ? value.name : undefined;\n\tconst reason = typeof value.reason === \"string\" ? value.reason : undefined;\n\n\tif (!name) return null;\n\treturn { name, reason };\n}\n\nfunction normalizeEntityMatches(value: unknown): EntityMatch[] {\n\tif (Array.isArray(value)) {\n\t\treturn value\n\t\t\t.map((entry) => normalizeEntityMatch(entry))\n\t\t\t.filter((entry): entry is EntityMatch => entry !== null);\n\t}\n\n\tif (isRecord(value) && \"match\" in value) {\n\t\treturn normalizeEntityMatches(value.match);\n\t}\n\n\tconst directMatch = normalizeEntityMatch(value);\n\treturn directMatch ? [directMatch] : [];\n}\n\nfunction parseEntityResolutionResponse(\n\ttext: string,\n): (ParsedResolution & { type?: string; entityId?: string }) | null {\n\tif (!text) return null;\n\n\tconst parsed = utils.parseKeyValueXml<Record<string, unknown>>(text);\n\tconst trimmed = text.trim();\n\n\tif (parsed) {\n\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : undefined;\n\t\tconst entityId =\n\t\t\ttypeof parsed.entityId === \"string\"\n\t\t\t\t? parsed.entityId\n\t\t\t\t: typeof parsed.resolvedId === \"string\"\n\t\t\t\t\t? parsed.resolvedId\n\t\t\t\t\t: undefined;\n\t\tconst matches = normalizeEntityMatches(parsed.matches);\n\n\t\tif (type || entityId || matches.length > 0) {\n\t\t\treturn {\n\t\t\t\ttype,\n\t\t\t\tentityId: entityId && entityId !== \"null\" ? entityId : undefined,\n\t\t\t\tmatches: matches.length > 0 ? { match: matches } : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\ttry {\n\t\tconst parsedJson = JSON.parse(trimmed) as unknown;\n\t\tif (parsedJson && typeof parsedJson === \"object\") {\n\t\t\tconst obj = parsedJson as Record<string, unknown>;\n\t\t\tconst type = typeof obj.type === \"string\" ? obj.type : undefined;\n\t\t\tconst entityId =\n\t\t\t\ttypeof obj.entityId === \"string\"\n\t\t\t\t\t? obj.entityId\n\t\t\t\t\t: typeof obj.resolvedId === \"string\"\n\t\t\t\t\t\t? obj.resolvedId\n\t\t\t\t\t\t: undefined;\n\t\t\tconst matches = normalizeEntityMatches(obj.matches);\n\n\t\t\tif (type || entityId || matches.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\ttype,\n\t\t\t\t\tentityId: entityId && entityId !== \"null\" ? entityId : undefined,\n\t\t\t\t\tmatches: matches.length > 0 ? { match: matches } : undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// ignore\n\t}\n\n\treturn null;\n}\n\nconst entityResolutionTemplate = `# Task: Resolve Entity Name\nMessage Sender: {{senderName}} (ID: {{senderId}})\nAgent: {{agentName}} (ID: {{agentId}})\n\n# Entities in Room:\n{{#if entitiesInRoom}}\n{{entitiesInRoom}}\n{{/if}}\n\n{{recentMessages}}\n\n# Instructions:\n1. Analyze the context to identify which entity is being referenced\n2. Consider special references like \"me\" (the message sender) or \"you\" (agent the message is directed to)\n3. Look for usernames/handles in standard formats (e.g. @username, user#1234)\n4. Consider context from recent messages for pronouns and references\n5. If multiple matches exist, use context to disambiguate\n6. Consider recent interactions and relationship strength when resolving ambiguity\n\nReturn a TOON document with:\nentityId: exact-id-if-known-otherwise-null\ntype: EXACT_MATCH | USERNAME_MATCH | NAME_MATCH | RELATIONSHIP_MATCH | AMBIGUOUS | UNKNOWN\nmatches[0]:\n name: matched-name\n reason: why this entity matches\n\nIMPORTANT: Your response must ONLY contain the TOON document above. Do not include any text, thinking, or reasoning before or after it.`;\n\nasync function getRecentInteractions(\n\truntime: IAgentRuntime,\n\tsourceEntityId: UUID,\n\tcandidateEntities: Entity[],\n\troomId: UUID,\n\trelationships: Relationship[],\n): Promise<{ entity: Entity; interactions: Memory[]; count: number }[]> {\n\tconst results: Array<{\n\t\tentity: Entity;\n\t\tinteractions: Memory[];\n\t\tcount: number;\n\t}> = [];\n\n\tconst recentMessages = await runtime.getMemories({\n\t\ttableName: \"messages\",\n\t\troomId,\n\t\tlimit: 20,\n\t});\n\tconst messageEntityById = new Map<UUID, UUID>();\n\tfor (const recentMessage of recentMessages) {\n\t\tif (recentMessage.id && recentMessage.entityId) {\n\t\t\tmessageEntityById.set(recentMessage.id, recentMessage.entityId);\n\t\t}\n\t}\n\n\tfor (const entity of candidateEntities) {\n\t\tconst interactions: Memory[] = [];\n\t\tlet interactionScore = 0;\n\n\t\tconst directReplies = recentMessages.filter((msg) => {\n\t\t\tif (!msg.entityId || !msg.content.inReplyTo) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst repliedToEntityId = messageEntityById.get(msg.content.inReplyTo);\n\t\t\treturn (\n\t\t\t\t(msg.entityId === sourceEntityId && repliedToEntityId === entity.id) ||\n\t\t\t\t(msg.entityId === entity.id && repliedToEntityId === sourceEntityId)\n\t\t\t);\n\t\t});\n\n\t\tinteractions.push(...directReplies);\n\n\t\tconst relationship = relationships.find(\n\t\t\t(rel) =>\n\t\t\t\t(rel.sourceEntityId === sourceEntityId &&\n\t\t\t\t\trel.targetEntityId === entity.id) ||\n\t\t\t\t(rel.targetEntityId === sourceEntityId &&\n\t\t\t\t\trel.sourceEntityId === entity.id),\n\t\t);\n\n\t\tconst relationshipMetadata = relationship?.metadata;\n\t\tif (relationshipMetadata?.interactions) {\n\t\t\tinteractionScore = relationshipMetadata.interactions as number;\n\t\t}\n\n\t\tinteractionScore += directReplies.length;\n\n\t\tconst uniqueInteractions = [...new Set(interactions)];\n\t\tresults.push({\n\t\t\tentity,\n\t\t\tinteractions: uniqueInteractions.slice(-5),\n\t\t\tcount: Math.round(interactionScore),\n\t\t});\n\t}\n\n\treturn results.sort((a, b) => b.count - a.count);\n}\n\nexport async function findEntityByName(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n\tstate: State,\n): Promise<Entity | null> {\n\tconst room = state.data.room ?? (await runtime.getRoom(message.roomId));\n\tif (!room) {\n\t\tlogger.warn(\n\t\t\t{ src: \"core:entities\", roomId: message.roomId },\n\t\t\t\"Room not found for entity search\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst world: World | null = room.worldId\n\t\t? await runtime.getWorld(room.worldId)\n\t\t: null;\n\n\tconst entitiesInRoom = await runtime.getEntitiesForRoom(room.id, true);\n\n\tconst filteredEntities = await Promise.all(\n\t\tentitiesInRoom.map(async (entity) => {\n\t\t\tif (!entity.components) return entity;\n\n\t\t\tconst worldMetadata = world?.metadata;\n\t\t\tconst worldRoles = worldMetadata?.roles || {};\n\n\t\t\tentity.components = entity.components.filter((component) => {\n\t\t\t\tif (component.sourceEntityId === message.entityId) return true;\n\n\t\t\t\tif (world && component.sourceEntityId) {\n\t\t\t\t\tconst sourceRole = worldRoles[component.sourceEntityId];\n\t\t\t\t\tif (sourceRole === \"OWNER\" || sourceRole === \"ADMIN\") return true;\n\t\t\t\t}\n\n\t\t\t\tif (component.sourceEntityId === runtime.agentId) return true;\n\n\t\t\t\treturn false;\n\t\t\t});\n\n\t\t\treturn entity;\n\t\t}),\n\t);\n\n\tconst relationships = await runtime.getRelationships({\n\t\tentityIds: [message.entityId],\n\t});\n\n\tconst relationshipEntities = await Promise.all(\n\t\trelationships.map(async (rel) => {\n\t\t\tconst entityId =\n\t\t\t\trel.sourceEntityId === message.entityId\n\t\t\t\t\t? rel.targetEntityId\n\t\t\t\t\t: rel.sourceEntityId;\n\t\t\treturn runtime.getEntityById(entityId);\n\t\t}),\n\t);\n\n\tconst allEntities = [\n\t\t...filteredEntities,\n\t\t...relationshipEntities.filter((e): e is Entity => e !== null),\n\t];\n\n\tconst interactionData = await getRecentInteractions(\n\t\truntime,\n\t\tmessage.entityId,\n\t\tallEntities,\n\t\troom.id as UUID,\n\t\trelationships,\n\t);\n\n\tconst prompt = utils.composePrompt({\n\t\tstate: {\n\t\t\troomName: (room.name || room.id) as string,\n\t\t\tworldName: (world?.name || \"Unknown\") as string,\n\t\t\tentitiesInRoom: JSON.stringify(filteredEntities, null, 2),\n\t\t\tentityId: message.entityId,\n\t\t\tsenderId: message.entityId,\n\t\t},\n\t\ttemplate: entityResolutionTemplate,\n\t});\n\n\tconst result = await runtime.useModel(ModelType.TEXT_SMALL, {\n\t\tprompt,\n\t\tstopSequences: [],\n\t});\n\n\tconst resolution = parseEntityResolutionResponse(result);\n\tif (!resolution) {\n\t\t// If the model output is malformed, fall back to a conservative heuristic:\n\t\t// when there's only one candidate entity in context, return it.\n\t\tif (filteredEntities.length === 1) {\n\t\t\treturn filteredEntities[0] ?? null;\n\t\t}\n\t\tlogger.warn(\n\t\t\t{ src: \"core:entities\" },\n\t\t\t\"Failed to parse entity resolution result\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tif (resolution.type === \"EXACT_MATCH\" && resolution.entityId) {\n\t\tconst entity = await runtime.getEntityById(resolution.entityId as UUID);\n\t\tif (entity) {\n\t\t\tif (entity.components) {\n\t\t\t\tconst worldMetadata = world?.metadata;\n\t\t\t\tconst worldRoles = worldMetadata?.roles || {};\n\t\t\t\tentity.components = entity.components.filter((component) => {\n\t\t\t\t\tif (component.sourceEntityId === message.entityId) return true;\n\t\t\t\t\tif (world && component.sourceEntityId) {\n\t\t\t\t\t\tconst sourceRole = worldRoles[component.sourceEntityId];\n\t\t\t\t\t\tif (sourceRole === \"OWNER\" || sourceRole === \"ADMIN\") return true;\n\t\t\t\t\t}\n\t\t\t\t\tif (component.sourceEntityId === runtime.agentId) return true;\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn entity;\n\t\t}\n\t}\n\n\tlet matchesArray: EntityMatch[] = [];\n\tconst parsedResolution = resolution as ParsedResolution;\n\tconst parsedResolutionMatches = parsedResolution.matches;\n\tif (parsedResolutionMatches?.match) {\n\t\tconst matchValue = parsedResolutionMatches.match;\n\t\tmatchesArray = Array.isArray(matchValue) ? matchValue : [matchValue];\n\t}\n\n\tconst normalize = (s: string): string => s.trim().toLowerCase();\n\tconst stripAt = (s: string): string => normalize(s).replace(/^@+/, \"\");\n\tconst indexedEntities = allEntities.map((entity) => {\n\t\tconst normalizedNames = new Set<string>();\n\t\tconst strippedNames = new Set<string>();\n\t\tfor (const name of entity.names) {\n\t\t\tnormalizedNames.add(normalize(name));\n\t\t\tstrippedNames.add(stripAt(name));\n\t\t}\n\n\t\tconst normalizedUsernames = new Set<string>();\n\t\tconst strippedUsernames = new Set<string>();\n\t\tconst normalizedHandles = new Set<string>();\n\t\tconst strippedHandles = new Set<string>();\n\t\tconst fallbackTokens: string[] = [];\n\t\tfor (const component of entity.components ?? []) {\n\t\t\tconst username =\n\t\t\t\ttypeof component.data?.username === \"string\"\n\t\t\t\t\t? component.data.username\n\t\t\t\t\t: undefined;\n\t\t\tif (username) {\n\t\t\t\tnormalizedUsernames.add(normalize(username));\n\t\t\t\tstrippedUsernames.add(stripAt(username));\n\t\t\t\tfallbackTokens.push(normalize(username));\n\t\t\t}\n\n\t\t\tconst handle =\n\t\t\t\ttypeof component.data?.handle === \"string\"\n\t\t\t\t\t? component.data.handle\n\t\t\t\t\t: undefined;\n\t\t\tif (handle) {\n\t\t\t\tconst normalizedHandle = normalize(handle);\n\t\t\t\tnormalizedHandles.add(normalizedHandle);\n\t\t\t\tstrippedHandles.add(stripAt(handle));\n\t\t\t\tfallbackTokens.push(normalizedHandle);\n\t\t\t\tconst handleNoAt = handle.replace(/^@+/, \"\");\n\t\t\t\tif (handleNoAt) {\n\t\t\t\t\tfallbackTokens.push(normalize(handleNoAt));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tentity,\n\t\t\tnormalizedNames,\n\t\t\tstrippedNames,\n\t\t\tnormalizedUsernames,\n\t\t\tstrippedUsernames,\n\t\t\tnormalizedHandles,\n\t\t\tstrippedHandles,\n\t\t\tfallbackTokens,\n\t\t};\n\t});\n\n\tconst firstMatch = matchesArray[0];\n\tif (matchesArray.length > 0 && firstMatch && firstMatch.name) {\n\t\tconst matchName = normalize(firstMatch.name);\n\t\tconst matchKey = stripAt(firstMatch.name);\n\n\t\tconst matchingEntity = indexedEntities.find((entry) => {\n\t\t\tif (\n\t\t\t\tentry.strippedNames.has(matchKey) ||\n\t\t\t\tentry.normalizedNames.has(matchName) ||\n\t\t\t\tentry.strippedUsernames.has(matchKey) ||\n\t\t\t\tentry.normalizedUsernames.has(matchName) ||\n\t\t\t\tentry.strippedHandles.has(matchKey) ||\n\t\t\t\tentry.normalizedHandles.has(matchName)\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t})?.entity;\n\n\t\tif (matchingEntity) {\n\t\t\tif (resolution.type === \"RELATIONSHIP_MATCH\") {\n\t\t\t\tconst interactionInfo = interactionData.find(\n\t\t\t\t\t(d) => d.entity.id === matchingEntity.id,\n\t\t\t\t);\n\t\t\t\tif (interactionInfo && interactionInfo.count > 0) {\n\t\t\t\t\treturn matchingEntity;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn matchingEntity;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: if parsing failed to produce a usable match list, try to detect\n\t// usernames/handles mentioned in the raw model output.\n\tconst resultLower = result.toLowerCase();\n\tconst fallbackEntity = indexedEntities.find((entry) =>\n\t\tentry.fallbackTokens.some((token) => resultLower.includes(token)),\n\t)?.entity;\n\tif (fallbackEntity) {\n\t\treturn fallbackEntity;\n\t}\n\n\t// Heuristic fallback: if the model indicates a name/username match but we\n\t// couldn't map it, and there's only a single candidate entity in context,\n\t// return it rather than failing closed.\n\tif (\n\t\t(resolution.type === \"USERNAME_MATCH\" ||\n\t\t\tresolution.type === \"NAME_MATCH\") &&\n\t\tfilteredEntities.length === 1\n\t) {\n\t\treturn filteredEntities[0] ?? null;\n\t}\n\n\t// Final fallback: if there's only one candidate entity in scope, return it.\n\t// This prevents needless nulls in small rooms when the model response is noisy.\n\tif (allEntities.length === 1) {\n\t\treturn allEntities[0] ?? null;\n\t}\n\n\treturn null;\n}\n\nexport const createUniqueUuid = (\n\truntime: IAgentRuntime,\n\tbaseUserId: UUID | string,\n): UUID => {\n\tif (baseUserId === runtime.agentId) {\n\t\treturn runtime.agentId;\n\t}\n\n\tconst combinedString = `${baseUserId}:${runtime.agentId}`;\n\treturn utils.stringToUuid(combinedString);\n};\n\nexport async function getEntityDetails({\n\truntime,\n\troomId,\n}: {\n\truntime: IAgentRuntime;\n\troomId: UUID;\n}) {\n\tconst runtimeCache = entityDetailsCache.get(runtime) ?? new Map();\n\tentityDetailsCache.set(runtime, runtimeCache);\n\n\tconst cacheKey = String(roomId);\n\tconst cachedEntry = runtimeCache.get(cacheKey);\n\tif (cachedEntry && cachedEntry.expiresAt > Date.now()) {\n\t\treturn cachedEntry.promise;\n\t}\n\n\tconst pendingPromise = (async () => {\n\t\tconst [room, roomEntities] = await Promise.all([\n\t\t\truntime.getRoom(roomId),\n\t\t\truntime.getEntitiesForRoom(roomId, true),\n\t\t]);\n\n\t\tconst uniqueEntities = new Map<string, EntityDetailsRecord>();\n\n\t\tfor (const entity of roomEntities) {\n\t\t\tconst entityId = entity.id;\n\t\t\tif (!entityId || uniqueEntities.has(entityId)) continue;\n\n\t\t\tconst allData = {};\n\t\t\tfor (const component of entity.components || []) {\n\t\t\t\tObject.assign(allData, component.data);\n\t\t\t}\n\n\t\t\tconst mergedData: Record<string, unknown> = {};\n\t\t\tfor (const [key, value] of Object.entries(allData)) {\n\t\t\t\tif (!mergedData[key]) {\n\t\t\t\t\tmergedData[key] = value;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(mergedData[key]) && Array.isArray(value)) {\n\t\t\t\t\tmergedData[key] = [...new Set([...mergedData[key], ...value])];\n\t\t\t\t} else if (\n\t\t\t\t\ttypeof mergedData[key] === \"object\" &&\n\t\t\t\t\ttypeof value === \"object\"\n\t\t\t\t) {\n\t\t\t\t\tmergedData[key] = { ...mergedData[key], ...value };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst getEntityNameFromMetadata = (\n\t\t\t\tsource: string,\n\t\t\t): string | undefined => {\n\t\t\t\tconst sourceMetadata = entity.metadata?.[source];\n\t\t\t\tif (\n\t\t\t\t\tsourceMetadata &&\n\t\t\t\t\ttypeof sourceMetadata === \"object\" &&\n\t\t\t\t\tsourceMetadata !== null\n\t\t\t\t) {\n\t\t\t\t\tconst metadataObj = sourceMetadata as Record<string, unknown>;\n\t\t\t\t\tif (\"name\" in metadataObj && typeof metadataObj.name === \"string\") {\n\t\t\t\t\t\treturn metadataObj.name;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t};\n\n\t\t\tuniqueEntities.set(entityId, {\n\t\t\t\tid: entityId,\n\t\t\t\tname: room?.source\n\t\t\t\t\t? getEntityNameFromMetadata(String(room.source)) || entity.names[0]\n\t\t\t\t\t: entity.names[0],\n\t\t\t\tnames: entity.names,\n\t\t\t\tdata: stableStringify({ ...mergedData, ...entity.metadata }),\n\t\t\t});\n\t\t}\n\n\t\treturn Array.from(uniqueEntities.values()).sort((left, right) => {\n\t\t\tconst leftName = left.name ?? left.names[0] ?? \"\";\n\t\t\tconst rightName = right.name ?? right.names[0] ?? \"\";\n\t\t\treturn (\n\t\t\t\tleftName.localeCompare(rightName) ||\n\t\t\t\tString(left.id ?? \"\").localeCompare(String(right.id ?? \"\"))\n\t\t\t);\n\t\t});\n\t})();\n\n\truntimeCache.set(cacheKey, {\n\t\texpiresAt: Date.now() + ENTITY_DETAILS_CACHE_TTL_MS,\n\t\tpromise: pendingPromise,\n\t});\n\n\ttry {\n\t\treturn await pendingPromise;\n\t} catch (error) {\n\t\truntimeCache.delete(cacheKey);\n\t\tthrow error;\n\t}\n}\n\nexport function formatEntities({ entities }: { entities: Entity[] }) {\n\tconst sortedEntities = [...entities].sort((left, right) => {\n\t\tconst leftName = left.names[0] ?? \"\";\n\t\tconst rightName = right.names[0] ?? \"\";\n\t\treturn (\n\t\t\tleftName.localeCompare(rightName) ||\n\t\t\tString(left.id ?? \"\").localeCompare(String(right.id ?? \"\"))\n\t\t);\n\t});\n\n\tconst entityStrings = sortedEntities.map((entity: Entity) => {\n\t\tconst header = `\"${entity.names.join('\" aka \"')}\"\\nID: ${entity.id}${\n\t\t\tentity.metadata && Object.keys(entity.metadata).length > 0\n\t\t\t\t? `\\nData: ${stableStringify(entity.metadata)}\\n`\n\t\t\t\t: \"\\n\"\n\t\t}`;\n\t\treturn header;\n\t});\n\treturn entityStrings.join(\"\\n\");\n}\n",
|
|
463
463
|
"import { createUniqueUuid } from \"./entities\";\nimport { logger } from \"./logger\";\nimport type { IAgentRuntime, Memory, Role, UUID, World } from \"./types\";\n\nconst DEFAULT_SERVER_ROLE: Role = \"NONE\";\n\nexport type RoleName = \"OWNER\" | \"ADMIN\" | \"USER\" | \"GUEST\";\n\nexport type RoleGrantSource = \"owner\" | \"manual\" | \"connector_admin\";\n\nexport const ROLE_RANK: Record<RoleName, number> = {\n\tGUEST: 0,\n\tUSER: 1,\n\tADMIN: 2,\n\tOWNER: 3,\n};\n\nexport type RolesWorldMetadata = {\n\townership?: { ownerId?: string };\n\troles?: Record<string, RoleName>;\n\troleSources?: Record<string, RoleGrantSource>;\n};\n\nexport type ConnectorAdminWhitelist = Record<string, string[]>;\n\nexport type RolesConfig = {\n\tconnectorAdmins?: ConnectorAdminWhitelist;\n};\n\nexport type RoleCheckResult = {\n\tentityId: UUID;\n\trole: RoleName;\n\tisOwner: boolean;\n\tisAdmin: boolean;\n\tcanManageRoles: boolean;\n};\n\nexport interface ServerOwnershipState {\n\tservers: {\n\t\t[serverId: string]: World;\n\t};\n}\n\nconst CONNECTOR_ADMINS_SETTING_KEY = \"ELIZA_ROLES_CONNECTOR_ADMINS_JSON\";\nconst CANONICAL_OWNER_SETTING_KEY = \"ELIZA_ADMIN_ENTITY_ID\";\nconst OWNER_CONTACTS_SETTING_KEY = \"ELIZA_OWNER_CONTACTS_JSON\";\nconst CONNECTOR_ID_FIELDS = [\"userId\", \"id\", \"username\", \"userName\"] as const;\nconst CONNECTOR_STABLE_ID_FIELDS = [\"userId\", \"id\"] as const;\ntype ConnectorIdField = (typeof CONNECTOR_ID_FIELDS)[number];\ntype ConnectorAdminMatch = {\n\tconnector: string;\n\tmatchedValue: string;\n\tmatchedField: ConnectorIdField;\n};\n\ntype ResolveEntityRoleOptions = {\n\tliveEntityMetadata?: Record<string, unknown> | null;\n\tliveEntityId?: string;\n};\n\ntype OwnerContactEntry = {\n\tentityId?: string;\n};\n\nfunction asStringArray(value: unknown): string[] {\n\tif (!Array.isArray(value)) return [];\n\treturn value\n\t\t.filter((entry): entry is string => typeof entry === \"string\")\n\t\t.map((entry) => entry.trim())\n\t\t.filter(Boolean);\n}\n\nfunction normalizeConnectorAdminWhitelist(\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): ConnectorAdminWhitelist {\n\tif (!whitelist || typeof whitelist !== \"object\") return {};\n\n\treturn Object.fromEntries(\n\t\tObject.entries(whitelist)\n\t\t\t.map(([connector, values]) => [connector, asStringArray(values)])\n\t\t\t.filter(([, values]) => values.length > 0),\n\t);\n}\n\nfunction normalizeRoleGrantSource(\n\traw: string | undefined | null,\n): RoleGrantSource | null {\n\tif (raw === \"owner\" || raw === \"manual\" || raw === \"connector_admin\") {\n\t\treturn raw;\n\t}\n\treturn null;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction formatError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction getRuntimeSettingString(\n\truntime: IAgentRuntime,\n\tkey: string,\n): string | undefined {\n\tif (typeof runtime.getSetting !== \"function\") {\n\t\treturn undefined;\n\t}\n\n\tconst value = runtime.getSetting(key);\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction parseOwnerContactEntityIds(raw: string | undefined): string[] {\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Record<string, OwnerContactEntry>;\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\t\treturn [];\n\t\t}\n\n\t\treturn Object.values(parsed)\n\t\t\t.map((entry) =>\n\t\t\t\tentry && typeof entry.entityId === \"string\"\n\t\t\t\t\t? entry.entityId.trim()\n\t\t\t\t\t: \"\",\n\t\t\t)\n\t\t\t.filter((entityId) => entityId.length > 0);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to parse owner contacts from runtime settings: ${formatError(error)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\nfunction getMemoryMetadata(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\treturn asRecord((message as Memory & { metadata?: unknown }).metadata);\n}\n\nfunction getMessageSource(message: Memory): string | undefined {\n\treturn typeof message.content?.source === \"string\"\n\t\t? message.content.source\n\t\t: undefined;\n}\n\nfunction getConnectorMetadataFromMemory(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\tconst memoryMetadata = getMemoryMetadata(message);\n\tconst source = getMessageSource(message);\n\tif (!source) {\n\t\treturn undefined;\n\t}\n\n\tconst sourceMetadata = asRecord(memoryMetadata?.[source]);\n\tif (sourceMetadata) {\n\t\treturn { [source]: sourceMetadata };\n\t}\n\n\tif (source === \"discord\") {\n\t\tconst fromId = memoryMetadata?.fromId;\n\t\tif (typeof fromId !== \"string\" || fromId.trim().length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst entityName =\n\t\t\ttypeof memoryMetadata?.entityName === \"string\"\n\t\t\t\t? memoryMetadata.entityName\n\t\t\t\t: undefined;\n\n\t\treturn {\n\t\t\tdiscord: {\n\t\t\t\tuserId: fromId,\n\t\t\t\tid: fromId,\n\t\t\t\t...(entityName ? { name: entityName, username: entityName } : {}),\n\t\t\t},\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\nasync function getEntityMetadata(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<Record<string, unknown> | undefined> {\n\tif (typeof runtime.getEntityById !== \"function\") {\n\t\treturn undefined;\n\t}\n\n\ttry {\n\t\tconst entity = await runtime.getEntityById(entityId as UUID);\n\t\treturn asRecord(entity?.metadata);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to look up entity ${entityId}: ${formatError(error)}`,\n\t\t);\n\t\treturn undefined;\n\t}\n}\n\nexport async function getUserServerRole(\n\truntime: IAgentRuntime,\n\tentityId: string,\n\tserverId: string,\n): Promise<Role> {\n\tconst worldId = createUniqueUuid(runtime, serverId);\n\tconst world = await runtime.getWorld(worldId);\n\n\tconst worldMetadata = world?.metadata;\n\tconst roles = worldMetadata?.roles;\n\tif (!roles) {\n\t\treturn DEFAULT_SERVER_ROLE;\n\t}\n\n\tconst role = roles[entityId as UUID];\n\tif (role) {\n\t\treturn role;\n\t}\n\n\treturn DEFAULT_SERVER_ROLE;\n}\n\nexport async function findWorldsForOwner(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<World[] | null> {\n\tif (!entityId) {\n\t\tlogger.error(\n\t\t\t{ src: \"core:roles\", agentId: runtime.agentId },\n\t\t\t\"User ID is required to find server\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst worlds = await runtime.getAllWorlds();\n\n\tif (!worlds || worlds.length === 0) {\n\t\tlogger.debug(\n\t\t\t{ src: \"core:roles\", agentId: runtime.agentId },\n\t\t\t\"No worlds found for agent\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst ownerWorlds: World[] = [];\n\tfor (const world of worlds) {\n\t\tconst worldMetadata = world.metadata;\n\t\tconst worldMetadataOwnership = worldMetadata?.ownership;\n\t\tif (worldMetadataOwnership && worldMetadataOwnership.ownerId === entityId) {\n\t\t\townerWorlds.push(world);\n\t\t}\n\t}\n\n\treturn ownerWorlds.length ? ownerWorlds : null;\n}\n\nexport function getConfiguredOwnerEntityIds(runtime: IAgentRuntime): string[] {\n\tconst configuredAdminEntityId = getRuntimeSettingString(\n\t\truntime,\n\t\tCANONICAL_OWNER_SETTING_KEY,\n\t);\n\tconst ownerContactsRaw = getRuntimeSettingString(\n\t\truntime,\n\t\tOWNER_CONTACTS_SETTING_KEY,\n\t);\n\tconst ownerContactEntityIds = parseOwnerContactEntityIds(ownerContactsRaw);\n\tconst deduped = new Set<string>();\n\n\tif (configuredAdminEntityId) {\n\t\tdeduped.add(configuredAdminEntityId);\n\t}\n\n\tfor (const entityId of ownerContactEntityIds) {\n\t\tdeduped.add(entityId);\n\t}\n\n\treturn [...deduped];\n}\n\nexport function hasConfiguredCanonicalOwner(runtime: IAgentRuntime): boolean {\n\treturn getConfiguredOwnerEntityIds(runtime).length > 0;\n}\n\nexport function resolveCanonicalOwnerId(\n\truntime: IAgentRuntime,\n\tmetadata?: RolesWorldMetadata,\n): string | null {\n\tconst configuredOwnerIds = getConfiguredOwnerEntityIds(runtime);\n\tif (configuredOwnerIds.length > 0) {\n\t\treturn configuredOwnerIds[0] ?? null;\n\t}\n\n\tconst worldOwnerId = metadata?.ownership?.ownerId;\n\treturn typeof worldOwnerId === \"string\" && worldOwnerId.length > 0\n\t\t? worldOwnerId\n\t\t: null;\n}\n\nfunction resolveOwnershipCandidateIds(\n\truntime: IAgentRuntime,\n\tmetadata?: RolesWorldMetadata,\n): string[] {\n\tconst configuredOwnerIds = getConfiguredOwnerEntityIds(runtime);\n\tif (configuredOwnerIds.length > 0) {\n\t\treturn configuredOwnerIds;\n\t}\n\n\tconst ownerId = resolveCanonicalOwnerId(runtime, metadata);\n\treturn ownerId ? [ownerId] : [];\n}\n\nfunction connectorIdentityMatches(\n\tleft: Record<string, unknown> | null | undefined,\n\tright: Record<string, unknown> | null | undefined,\n): boolean {\n\tif (!left || !right) return false;\n\n\tfor (const [connector, leftRaw] of Object.entries(left)) {\n\t\tconst leftConnector = asRecord(leftRaw);\n\t\tconst rightConnector = asRecord(right[connector]);\n\t\tif (!leftConnector || !rightConnector) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const field of CONNECTOR_STABLE_ID_FIELDS) {\n\t\t\tconst leftValue = leftConnector[field];\n\t\t\tconst rightValue = rightConnector[field];\n\t\t\tif (\n\t\t\t\ttypeof leftValue === \"string\" &&\n\t\t\t\tleftValue.length > 0 &&\n\t\t\t\tleftValue === rightValue\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false;\n}\n\nasync function hasConfirmedIdentityLink(\n\truntime: IAgentRuntime,\n\tentityId: string,\n\townerId: string,\n): Promise<boolean> {\n\tconst linkedIds = await getConfirmedLinkedEntityIds(runtime, entityId);\n\treturn linkedIds.includes(ownerId);\n}\n\nasync function getConfirmedLinkedEntityIds(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<string[]> {\n\tif (typeof runtime.getRelationships !== \"function\") {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst relationships = await runtime.getRelationships({\n\t\t\tentityIds: [entityId as UUID],\n\t\t\ttags: [\"identity_link\"],\n\t\t});\n\n\t\tconst linkedIds = new Set<string>();\n\t\tfor (const relationship of relationships) {\n\t\t\tconst metadata = asRecord(relationship.metadata);\n\t\t\tif (metadata?.status !== \"confirmed\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\trelationship.sourceEntityId === entityId &&\n\t\t\t\ttypeof relationship.targetEntityId === \"string\"\n\t\t\t) {\n\t\t\t\tlinkedIds.add(relationship.targetEntityId);\n\t\t\t}\n\t\t\tif (\n\t\t\t\trelationship.targetEntityId === entityId &&\n\t\t\t\ttypeof relationship.sourceEntityId === \"string\"\n\t\t\t) {\n\t\t\t\tlinkedIds.add(relationship.sourceEntityId);\n\t\t\t}\n\t\t}\n\n\t\treturn [...linkedIds];\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to load identity links for ${entityId}: ${formatError(error)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\nasync function resolveOwnershipRole(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleName | null> {\n\tconst ownerIds = resolveOwnershipCandidateIds(runtime, metadata);\n\tif (ownerIds.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst senderMetadata =\n\t\toptions?.liveEntityMetadata ?? (await getEntityMetadata(runtime, entityId));\n\n\tfor (const ownerId of ownerIds) {\n\t\tif (ownerId === entityId) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\n\t\tif (await hasConfirmedIdentityLink(runtime, entityId, ownerId)) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\n\t\tconst ownerMetadata = await getEntityMetadata(runtime, ownerId);\n\t\tif (!ownerMetadata) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (connectorIdentityMatches(senderMetadata, ownerMetadata)) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction resolveWorldIdFromMessageMetadata(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): UUID | null {\n\tconst source = getMessageSource(message);\n\tconst metadata = getMemoryMetadata(message);\n\tif (source === \"discord\") {\n\t\tconst serverId =\n\t\t\ttypeof metadata?.discordServerId === \"string\"\n\t\t\t\t? metadata.discordServerId\n\t\t\t\t: typeof metadata?.discordChannelId === \"string\"\n\t\t\t\t\t? metadata.discordChannelId\n\t\t\t\t\t: null;\n\n\t\tif (!serverId) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn createUniqueUuid(runtime, serverId) as UUID;\n\t}\n\n\treturn null;\n}\n\nexport function setConnectorAdminWhitelist(\n\truntime: IAgentRuntime,\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): void {\n\tif (typeof runtime.setSetting !== \"function\") {\n\t\treturn;\n\t}\n\n\tconst normalized = normalizeConnectorAdminWhitelist(whitelist);\n\tif (Object.keys(normalized).length === 0) {\n\t\truntime.setSetting(CONNECTOR_ADMINS_SETTING_KEY, null);\n\t\treturn;\n\t}\n\n\truntime.setSetting(CONNECTOR_ADMINS_SETTING_KEY, JSON.stringify(normalized));\n}\n\nexport function getConnectorAdminWhitelist(\n\truntime: IAgentRuntime,\n): ConnectorAdminWhitelist {\n\tconst raw = getRuntimeSettingString(runtime, CONNECTOR_ADMINS_SETTING_KEY);\n\tif (!raw) {\n\t\treturn {};\n\t}\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Record<string, unknown>;\n\t\treturn normalizeConnectorAdminWhitelist(parsed);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to parse ${CONNECTOR_ADMINS_SETTING_KEY}: ${formatError(error)}`,\n\t\t);\n\t\treturn {};\n\t}\n}\n\nexport function matchEntityToConnectorAdminWhitelist(\n\tentityMetadata: Record<string, unknown> | null | undefined,\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): ConnectorAdminMatch | null {\n\tif (!entityMetadata || typeof entityMetadata !== \"object\") return null;\n\n\tconst normalizedWhitelist = normalizeConnectorAdminWhitelist(whitelist);\n\tfor (const [connector, platformIds] of Object.entries(normalizedWhitelist)) {\n\t\tconst connectorMeta = asRecord(entityMetadata[connector]);\n\t\tif (!connectorMeta) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const field of CONNECTOR_ID_FIELDS) {\n\t\t\tconst value = connectorMeta[field];\n\t\t\tif (typeof value === \"string\" && platformIds.includes(value)) {\n\t\t\t\treturn { connector, matchedValue: value, matchedField: field };\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport function normalizeRole(raw: string | undefined | null): RoleName {\n\tconst upper = (raw ?? \"\").toUpperCase();\n\tif (upper === \"OWNER\" || upper === \"ADMIN\" || upper === \"USER\") return upper;\n\treturn \"GUEST\";\n}\n\nexport function getEntityRole(\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n): RoleName {\n\tif (!metadata?.roles) return \"GUEST\";\n\treturn normalizeRole(metadata.roles[entityId]);\n}\n\nfunction getStoredRoleSource(\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n): RoleGrantSource | null {\n\treturn normalizeRoleGrantSource(metadata?.roleSources?.[entityId]);\n}\n\nasync function resolveStoredRoleSource(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleGrantSource | null> {\n\tconst storedSource = getStoredRoleSource(metadata, entityId);\n\tif (storedSource) {\n\t\treturn storedSource;\n\t}\n\n\tconst storedRole = getEntityRole(metadata, entityId);\n\tif (storedRole === \"GUEST\") {\n\t\treturn null;\n\t}\n\tif (storedRole === \"OWNER\") {\n\t\treturn \"owner\";\n\t}\n\n\tconst entityMetadata =\n\t\toptions?.liveEntityId === entityId\n\t\t\t? (options.liveEntityMetadata ?? undefined)\n\t\t\t: undefined;\n\tconst matchedWhitelist = matchEntityToConnectorAdminWhitelist(\n\t\tentityMetadata ?? (await getEntityMetadata(runtime, entityId)),\n\t\tgetConnectorAdminWhitelist(runtime),\n\t);\n\n\tif (storedRole === \"ADMIN\" && matchedWhitelist) {\n\t\treturn \"connector_admin\";\n\t}\n\n\treturn \"manual\";\n}\n\nasync function resolveExplicitGrantedRole(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<{\n\trole: RoleName;\n\tsource: \"manual\" | \"linked_manual\";\n} | null> {\n\tconst directRole = getEntityRole(metadata, entityId);\n\tconst directSource = await resolveStoredRoleSource(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tif (directRole !== \"GUEST\" && directSource === \"manual\") {\n\t\treturn { role: directRole, source: \"manual\" };\n\t}\n\n\tconst linkedIds = await getConfirmedLinkedEntityIds(runtime, entityId);\n\tlet bestRole: RoleName | null = null;\n\n\tfor (const linkedEntityId of linkedIds) {\n\t\tconst linkedRole = getEntityRole(metadata, linkedEntityId);\n\t\tif (linkedRole === \"GUEST\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst linkedSource = await resolveStoredRoleSource(\n\t\t\truntime,\n\t\t\tmetadata,\n\t\t\tlinkedEntityId,\n\t\t);\n\t\tif (linkedSource !== \"manual\") {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!bestRole || ROLE_RANK[linkedRole] > ROLE_RANK[bestRole]) {\n\t\t\tbestRole = linkedRole;\n\t\t}\n\t}\n\n\treturn bestRole ? { role: bestRole, source: \"linked_manual\" } : null;\n}\n\nexport function getLiveEntityMetadataFromMessage(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\t// Only trust connector identity stamped into the Memory itself.\n\t// content.metadata can come from untrusted chat clients, so it must not\n\t// participate in role resolution.\n\treturn getConnectorMetadataFromMemory(message);\n}\n\nexport async function resolveEntityRole(\n\truntime: IAgentRuntime,\n\t_world: Awaited<ReturnType<IAgentRuntime[\"getWorld\"]>>,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleName> {\n\tconst explicitRole = getEntityRole(metadata, entityId);\n\tconst explicitSource = await resolveStoredRoleSource(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tconst ownershipRole = await resolveOwnershipRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\tif (ownershipRole === \"OWNER\") {\n\t\treturn \"OWNER\";\n\t}\n\n\tconst whitelist = getConnectorAdminWhitelist(runtime);\n\tconst liveMatched = matchEntityToConnectorAdminWhitelist(\n\t\toptions?.liveEntityMetadata ?? undefined,\n\t\twhitelist,\n\t);\n\n\tif (explicitRole !== \"GUEST\") {\n\t\tif (explicitRole === \"OWNER\") {\n\t\t\treturn hasConfiguredCanonicalOwner(runtime) ? \"GUEST\" : \"OWNER\";\n\t\t}\n\n\t\tif (explicitSource === \"connector_admin\") {\n\t\t\tif (Object.keys(whitelist).length === 0) {\n\t\t\t\treturn \"GUEST\";\n\t\t\t}\n\n\t\t\tif (liveMatched) {\n\t\t\t\treturn \"ADMIN\";\n\t\t\t}\n\n\t\t\tconst entityMetadata = await getEntityMetadata(runtime, entityId);\n\t\t\tconst matched = matchEntityToConnectorAdminWhitelist(\n\t\t\t\tentityMetadata,\n\t\t\t\twhitelist,\n\t\t\t);\n\t\t\tif (matched) {\n\t\t\t\treturn \"ADMIN\";\n\t\t\t}\n\n\t\t\treturn \"GUEST\";\n\t\t}\n\n\t\treturn explicitRole;\n\t}\n\n\tif (Object.keys(whitelist).length === 0) {\n\t\treturn explicitRole;\n\t}\n\n\tif (liveMatched) {\n\t\treturn \"ADMIN\";\n\t}\n\n\tconst entityMetadata = await getEntityMetadata(runtime, entityId);\n\tconst matched = matchEntityToConnectorAdminWhitelist(\n\t\tentityMetadata,\n\t\twhitelist,\n\t);\n\tif (!matched) {\n\t\treturn explicitRole;\n\t}\n\n\treturn \"ADMIN\";\n}\n\nexport async function checkSenderPrivateAccess(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<{\n\tentityId: UUID;\n\trole: RoleName;\n\tisOwner: boolean;\n\tisAdmin: boolean;\n\tcanManageRoles: boolean;\n\thasPrivateAccess: boolean;\n\taccessRole: RoleName | null;\n\taccessSource: \"owner\" | \"manual\" | \"linked_manual\" | null;\n} | null> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) return null;\n\n\tconst { world, metadata } = resolved;\n\tconst entityId = message.entityId as UUID;\n\tconst options = {\n\t\tliveEntityMetadata: getLiveEntityMetadataFromMessage(message),\n\t\tliveEntityId: entityId,\n\t};\n\tconst role = await resolveEntityRole(\n\t\truntime,\n\t\tworld,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tconst ownershipRole = await resolveOwnershipRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\tif (ownershipRole === \"OWNER\") {\n\t\treturn {\n\t\t\tentityId,\n\t\t\trole,\n\t\t\tisOwner: true,\n\t\t\tisAdmin: true,\n\t\t\tcanManageRoles: true,\n\t\t\thasPrivateAccess: true,\n\t\t\taccessRole: \"OWNER\",\n\t\t\taccessSource: \"owner\",\n\t\t};\n\t}\n\n\tconst explicitAccess = await resolveExplicitGrantedRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\treturn {\n\t\tentityId,\n\t\trole,\n\t\tisOwner: false,\n\t\tisAdmin: role === \"OWNER\" || role === \"ADMIN\",\n\t\tcanManageRoles: role === \"OWNER\" || role === \"ADMIN\",\n\t\thasPrivateAccess: explicitAccess !== null,\n\t\taccessRole: explicitAccess?.role ?? null,\n\t\taccessSource: explicitAccess?.source ?? null,\n\t};\n}\n\nexport function canModifyRole(\n\tactorRole: RoleName,\n\ttargetCurrentRole: RoleName,\n\tnewRole: RoleName,\n): boolean {\n\tif (targetCurrentRole === newRole) return false;\n\tconst actorRank = ROLE_RANK[actorRole];\n\tconst targetRank = ROLE_RANK[targetCurrentRole];\n\tif (actorRole === \"OWNER\") return true;\n\tif (actorRole === \"ADMIN\") {\n\t\tif (targetRank >= actorRank) return false;\n\t\tif (newRole === \"OWNER\") return false;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport async function resolveWorldForMessage(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<{\n\tworld: Awaited<ReturnType<IAgentRuntime[\"getWorld\"]>>;\n\tmetadata: RolesWorldMetadata;\n} | null> {\n\tconst room = await runtime.getRoom(message.roomId);\n\tconst worldId =\n\t\troom?.worldId ?? resolveWorldIdFromMessageMetadata(runtime, message);\n\tif (!worldId) return null;\n\tconst world = await runtime.getWorld(worldId);\n\tif (!world) return null;\n\tconst metadata = (world.metadata ?? {}) as RolesWorldMetadata;\n\treturn { world, metadata };\n}\n\nexport async function resolveCanonicalOwnerIdForMessage(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<string | null> {\n\tconst configuredOwnerId = resolveCanonicalOwnerId(runtime);\n\tif (configuredOwnerId) {\n\t\treturn configuredOwnerId;\n\t}\n\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\treturn resolveCanonicalOwnerId(runtime, resolved?.metadata);\n}\n\nexport async function checkSenderRole(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<RoleCheckResult | null> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) return null;\n\tconst { world, metadata } = resolved;\n\tconst entityId = message.entityId as UUID;\n\tconst role = await resolveEntityRole(runtime, world, metadata, entityId, {\n\t\tliveEntityMetadata: getLiveEntityMetadataFromMessage(message),\n\t\tliveEntityId: entityId,\n\t});\n\treturn {\n\t\tentityId,\n\t\trole,\n\t\tisOwner: role === \"OWNER\",\n\t\tisAdmin: role === \"OWNER\" || role === \"ADMIN\",\n\t\tcanManageRoles: role === \"OWNER\" || role === \"ADMIN\",\n\t};\n}\n\nexport async function setEntityRole(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n\ttargetEntityId: string,\n\tnewRole: RoleName,\n\tsource: RoleGrantSource = \"manual\",\n): Promise<Record<string, RoleName>> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) throw new Error(\"Cannot resolve world for role assignment\");\n\tconst { world, metadata } = resolved;\n\tif (!metadata.roles) metadata.roles = {};\n\tmetadata.roleSources ??= {};\n\tmetadata.roles[targetEntityId] = newRole;\n\tif (newRole === \"GUEST\") {\n\t\tdelete metadata.roleSources[targetEntityId];\n\t} else {\n\t\tmetadata.roleSources[targetEntityId] = source;\n\t}\n\t(world as { metadata: RolesWorldMetadata }).metadata = metadata;\n\tawait runtime.updateWorld(\n\t\tworld as Parameters<IAgentRuntime[\"updateWorld\"]>[0],\n\t);\n\treturn { ...metadata.roles };\n}\n",
|
|
@@ -457,7 +457,7 @@
|
|
|
457
457
|
"import type { Metadata } from \"./primitives\";\nimport type { JsonValue } from \"./proto.js\";\nimport type { IAgentRuntime } from \"./runtime\";\n\n/**\n * Core service type registry that can be extended by plugins via module augmentation.\n * Plugins can extend this interface to add their own service types:\n *\n * @example\n * ```typescript\n * declare module '@elizaos/core' {\n * interface ServiceTypeRegistry {\n * MY_CUSTOM_SERVICE: 'my_custom_service';\n * }\n * }\n * ```\n */\nexport interface ServiceTypeRegistry {\n\tTRANSCRIPTION: \"transcription\";\n\tVIDEO: \"video\";\n\tBROWSER: \"browser\";\n\tPDF: \"pdf\";\n\tREMOTE_FILES: \"aws_s3\";\n\tWEB_SEARCH: \"web_search\";\n\tEMAIL: \"email\";\n\tTEE: \"tee\";\n\tTASK: \"task\";\n\tAPPROVAL: \"approval\";\n\tTOOL_POLICY: \"tool_policy\";\n\tWALLET: \"wallet\";\n\tLP_POOL: \"lp_pool\";\n\tTOKEN_DATA: \"token_data\";\n\tMESSAGE_SERVICE: \"message_service\";\n\tMESSAGE: \"message\";\n\tPOST: \"post\";\n\tHOOKS: \"hooks\";\n\tPAIRING: \"pairing\";\n\tAGENT_EVENT: \"agent_event\";\n\tOPTIMIZED_PROMPT: \"optimized_prompt\";\n\tUNKNOWN: \"unknown\";\n}\n\n/**\n * Type for service names that includes both core services and any plugin-registered services\n */\nexport type ServiceTypeName = ServiceTypeRegistry[keyof ServiceTypeRegistry];\n\n/**\n * Helper type to extract service type values from the registry\n */\nexport type ServiceTypeValue<K extends keyof ServiceTypeRegistry> =\n\tServiceTypeRegistry[K];\n\n/**\n * Helper type to check if a service type exists in the registry\n */\nexport type IsValidServiceType<T extends string> = T extends ServiceTypeName\n\t? true\n\t: false;\n\n/**\n * Type-safe service class definition\n */\nexport type TypedServiceClass<T extends ServiceTypeName> = {\n\tnew (runtime?: IAgentRuntime): Service;\n\tserviceType: T;\n\tstart(runtime: IAgentRuntime): Promise<Service>;\n};\n\n/**\n * Map of service type names to their implementation classes.\n * Plugins can extend this via module augmentation:\n * @example\n * ```typescript\n * declare module '@elizaos/core' {\n * interface ServiceClassMap {\n * MY_SERVICE: typeof MyService;\n * }\n * }\n * ```\n */\n// biome-ignore lint/complexity/noBannedTypes: Empty interface for module augmentation\nexport type ServiceClassMap = {};\n\n/**\n * Helper to infer service instance type from service type name\n */\nexport type ServiceInstance<T extends ServiceTypeName> =\n\tT extends keyof ServiceClassMap ? InstanceType<ServiceClassMap[T]> : Service;\n\n/**\n * Runtime service registry type\n */\nexport type ServiceRegistry<T extends ServiceTypeName = ServiceTypeName> = Map<\n\tT,\n\tService\n>;\n\n/**\n * Enumerates the recognized types of services that can be registered and used by the agent runtime.\n * Services provide specialized functionalities like audio transcription, video processing,\n * web browsing, PDF handling, file storage (e.g., AWS S3), web search, email integration,\n * secure execution via TEE (Trusted Execution Environment), and task management.\n * This constant is used in `AgentRuntime` for service registration and retrieval (e.g., `getService`).\n * Each service typically implements the `Service` abstract class or a more specific interface like `IVideoService`.\n */\nexport const ServiceType = {\n\tTRANSCRIPTION: \"transcription\",\n\tVIDEO: \"video\",\n\tBROWSER: \"browser\",\n\tPDF: \"pdf\",\n\tREMOTE_FILES: \"aws_s3\",\n\tWEB_SEARCH: \"web_search\",\n\tEMAIL: \"email\",\n\tTEE: \"tee\",\n\tTASK: \"task\",\n\tAPPROVAL: \"approval\",\n\tTOOL_POLICY: \"tool_policy\",\n\tWALLET: \"wallet\",\n\tLP_POOL: \"lp_pool\",\n\tTOKEN_DATA: \"token_data\",\n\tMESSAGE_SERVICE: \"message_service\",\n\tMESSAGE: \"message\",\n\tPOST: \"post\",\n\tHOOKS: \"hooks\",\n\tPAIRING: \"pairing\",\n\tAGENT_EVENT: \"agent_event\",\n\tVOICE_CACHE: \"voice_cache\",\n\tOPTIMIZED_PROMPT: \"optimized_prompt\",\n\tUNKNOWN: \"unknown\",\n} as const;\n\n/**\n * Client instance\n */\nexport abstract class Service {\n\t/** Runtime instance */\n\tprotected runtime!: IAgentRuntime;\n\n\tconstructor(runtime?: IAgentRuntime) {\n\t\tif (runtime) {\n\t\t\tthis.runtime = runtime;\n\t\t}\n\t}\n\n\tabstract stop(): Promise<void>;\n\n\t/** Service type */\n\tstatic serviceType: string;\n\n\t/** Service name */\n\tabstract capabilityDescription: string;\n\n\t/** Service configuration */\n\tconfig?: Metadata;\n\n\t/** Start service connection - subclasses must override this */\n\tstatic async start(_runtime: IAgentRuntime): Promise<Service> {\n\t\tthrow new Error(\"Service.start() must be implemented by subclass\");\n\t}\n\n\t/** Stop service connection - optional, subclasses may override this */\n\tstatic stopRuntime?(_runtime: IAgentRuntime): Promise<void>;\n\n\t/** Optional static method to register send handlers */\n\tstatic registerSendHandlers?(runtime: IAgentRuntime, service: Service): void;\n}\n\n/**\n * Generic service interface that provides better type checking for services\n * @template ConfigType The configuration type for this service\n * @template InputType The input type for processing\n * @template ResultType The result type returned by the service operations\n */\nexport interface TypedService<\n\tConfigType extends Metadata = Metadata,\n\tInputType = JsonValue,\n\tResultType = JsonValue,\n> extends Service {\n\t/**\n\t * The configuration for this service instance\n\t */\n\tconfig?: ConfigType;\n\n\t/**\n\t * Process an input with this service\n\t * @param input The input to process\n\t * @returns A promise resolving to the result\n\t */\n\tprocess(input: InputType): Promise<ResultType>;\n}\n\n/**\n * Generic factory function to create a typed service instance.\n * getService() is synchronous — no await needed.\n * @param runtime The agent runtime\n * @param serviceType The type of service to get\n * @returns The service instance or null if not available\n */\nexport function getTypedService<\n\tConfigType extends Metadata = Metadata,\n\tInputType = JsonValue,\n\tResultType = JsonValue,\n>(\n\truntime: IAgentRuntime,\n\tserviceType: ServiceTypeName,\n): TypedService<ConfigType, InputType, ResultType> | null {\n\treturn runtime.getService<TypedService<ConfigType, InputType, ResultType>>(\n\t\tserviceType,\n\t);\n}\n\n/**\n * Standardized service error type for consistent error handling\n */\nexport interface ServiceError {\n\tcode: string;\n\tmessage: string;\n\tdetails?: Record<string, JsonValue> | string | number | boolean | null;\n\tcause?: Error;\n}\n\n/**\n * Safely create a ServiceError from any caught error\n */\nexport function createServiceError(\n\terror: Error | string | JsonValue,\n\tcode = \"UNKNOWN_ERROR\",\n): ServiceError {\n\tif (error instanceof Error) {\n\t\treturn {\n\t\t\tcode,\n\t\t\tmessage: error.message,\n\t\t\tcause: error,\n\t\t};\n\t}\n\n\treturn {\n\t\tcode,\n\t\tmessage: String(error),\n\t};\n}\n",
|
|
458
458
|
"/**\n * Service Interface Definitions for elizaOS\n *\n * This module provides standardized service interface definitions that plugins implement.\n * Data types are proto-generated; runtime classes remain TypeScript.\n */\n\nimport type { Content, UUID } from \"./primitives\";\nimport type {\n\tJsonValue,\n\tLpPositionDetails,\n\tPoolInfo,\n\tTokenBalance,\n\tTokenData,\n\tTransactionResult,\n\tWalletAsset,\n\tWalletPortfolio,\n} from \"./proto.js\";\nimport { Service, ServiceType } from \"./service\";\n\nexport type {\n\tLpPositionDetails,\n\tPoolInfo,\n\tTokenBalance,\n\tTokenData,\n\tTransactionResult,\n\tWalletAsset,\n\tWalletPortfolio,\n};\n\n// ============================================================================\n// Message Bus Service Interface\n// ============================================================================\n\nexport interface IMessageBusService extends Service {\n\tnotifyActionStart(\n\t\troomId: UUID,\n\t\tworldId: UUID,\n\t\tcontent: Content,\n\t\tmessageId?: UUID,\n\t): Promise<void>;\n\n\tnotifyActionUpdate(\n\t\troomId: UUID,\n\t\tworldId: UUID,\n\t\tcontent: Content,\n\t\tmessageId?: UUID,\n\t): Promise<void>;\n}\n\n// ============================================================================\n// Token & Wallet Interfaces\n// ============================================================================\n\nexport abstract class ITokenDataService extends Service {\n\tstatic override readonly serviceType = ServiceType.TOKEN_DATA;\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to token market data.\" as string;\n\n\tabstract getTokenDetails(\n\t\taddress: string,\n\t\tchain: string,\n\t): Promise<TokenData | null>;\n\n\tabstract getTrendingTokens(\n\t\tchain?: string,\n\t\tlimit?: number,\n\t\ttimePeriod?: string,\n\t): Promise<TokenData[]>;\n\n\tabstract searchTokens(\n\t\tquery: string,\n\t\tchain?: string,\n\t\tlimit?: number,\n\t): Promise<TokenData[]>;\n\n\tabstract getTokensByAddresses(\n\t\taddresses: string[],\n\t\tchain: string,\n\t): Promise<TokenData[]>;\n}\n\nexport abstract class IWalletService extends Service {\n\tstatic override readonly serviceType = ServiceType.WALLET;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to wallet balances and portfolios.\";\n\n\tabstract getPortfolio(owner?: string): Promise<WalletPortfolio>;\n\n\tabstract getBalance(assetAddress: string, owner?: string): Promise<number>;\n\n\tabstract transferSol(\n\t\tfrom: object,\n\t\tto: object,\n\t\tlamports: number,\n\t): Promise<string>;\n}\n\n// ============================================================================\n// Liquidity Pool Interfaces\n// ============================================================================\n\nexport abstract class ILpService extends Service {\n\tstatic override readonly serviceType = \"lp_pool\";\n\n\tpublic readonly capabilityDescription =\n\t\t\"Provides standardized access to DEX liquidity pools.\";\n\n\tabstract getDexName(): string;\n\n\tabstract getPools(\n\t\ttokenAMint?: string,\n\t\ttokenBMint?: string,\n\t): Promise<PoolInfo[]>;\n\n\tabstract addLiquidity(params: {\n\t\tuserVault: object;\n\t\tpoolId: string;\n\t\ttokenAAmountLamports: string;\n\t\ttokenBAmountLamports?: string;\n\t\tslippageBps: number;\n\t\ttickLowerIndex?: number;\n\t\ttickUpperIndex?: number;\n\t}): Promise<TransactionResult & { lpTokensReceived?: TokenBalance }>;\n\n\tabstract removeLiquidity(params: {\n\t\tuserVault: object;\n\t\tpoolId: string;\n\t\tlpTokenAmountLamports: string;\n\t\tslippageBps: number;\n\t}): Promise<TransactionResult & { tokensReceived?: TokenBalance[] }>;\n\n\tabstract getLpPositionDetails(\n\t\tuserAccountPublicKey: string,\n\t\tpoolOrPositionIdentifier: string,\n\t): Promise<LpPositionDetails | null>;\n\n\tabstract getMarketDataForPools(\n\t\tpoolIds: string[],\n\t): Promise<Record<string, Partial<PoolInfo>>>;\n}\n\n// ============================================================================\n// Transcription & Audio Interfaces\n// ============================================================================\n\nexport abstract class ITranscriptionService extends Service {\n\tstatic override readonly serviceType = ServiceType.TRANSCRIPTION;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Audio transcription and speech processing capabilities\";\n\n\tabstract transcribeAudio(\n\t\taudioPath: string | Buffer,\n\t\toptions?: TranscriptionOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract transcribeVideo(\n\t\tvideoPath: string | Buffer,\n\t\toptions?: TranscriptionOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract speechToText(\n\t\taudioStream: NodeJS.ReadableStream | Buffer,\n\t\toptions?: SpeechToTextOptions,\n\t): Promise<TranscriptionResult>;\n\n\tabstract textToSpeech(\n\t\ttext: string,\n\t\toptions?: TextToSpeechOptions,\n\t): Promise<Buffer>;\n\n\tabstract getSupportedLanguages(): Promise<string[]>;\n\n\tabstract getAvailableVoices(): Promise<\n\t\tArray<{\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\tlanguage: string;\n\t\t\tgender?: \"male\" | \"female\" | \"neutral\";\n\t\t}>\n\t>;\n\n\tabstract detectLanguage(audioPath: string | Buffer): Promise<string>;\n}\n\n// ============================================================================\n// Video Interfaces\n// ============================================================================\n\nexport abstract class IVideoService extends Service {\n\tstatic override readonly serviceType = ServiceType.VIDEO;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Video download, processing, and conversion capabilities\";\n\n\tabstract getVideoInfo(url: string): Promise<VideoInfo>;\n\n\tabstract downloadVideo(\n\t\turl: string,\n\t\toptions?: VideoDownloadOptions,\n\t): Promise<string>;\n\n\tabstract extractAudio(\n\t\tvideoPath: string,\n\t\toutputPath?: string,\n\t): Promise<string>;\n\n\tabstract getThumbnail(videoPath: string, timestamp?: number): Promise<string>;\n\n\tabstract convertVideo(\n\t\tvideoPath: string,\n\t\toutputPath: string,\n\t\toptions?: VideoProcessingOptions,\n\t): Promise<string>;\n\n\tabstract getAvailableFormats(url: string): Promise<VideoFormat[]>;\n}\n\n// ============================================================================\n// Browser Interfaces\n// ============================================================================\n\nexport abstract class IBrowserService extends Service {\n\tstatic override readonly serviceType = ServiceType.BROWSER;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Web browser automation and scraping capabilities\";\n\n\tabstract navigate(\n\t\turl: string,\n\t\toptions?: BrowserNavigationOptions,\n\t): Promise<void>;\n\n\tabstract screenshot(options?: ScreenshotOptions): Promise<Buffer>;\n\n\tabstract extractContent(selector?: string): Promise<ExtractedContent>;\n\n\tabstract click(\n\t\tselector: string | ElementSelector,\n\t\toptions?: ClickOptions,\n\t): Promise<void>;\n\n\tabstract type(\n\t\tselector: string,\n\t\ttext: string,\n\t\toptions?: TypeOptions,\n\t): Promise<void>;\n\n\tabstract waitForElement(selector: string | ElementSelector): Promise<void>;\n\n\tabstract evaluate<T = JsonValue>(\n\t\tscript: string,\n\t\t...args: JsonValue[]\n\t): Promise<T>;\n\n\tabstract getCurrentUrl(): Promise<string>;\n\n\tabstract goBack(): Promise<void>;\n\n\tabstract goForward(): Promise<void>;\n\n\tabstract refresh(): Promise<void>;\n}\n\n// ============================================================================\n// PDF Interfaces\n// ============================================================================\n\nexport abstract class IPdfService extends Service {\n\tstatic override readonly serviceType = ServiceType.PDF;\n\n\tpublic readonly capabilityDescription =\n\t\t\"PDF processing, extraction, and generation capabilities\";\n\n\tabstract extractText(pdfPath: string | Buffer): Promise<PdfExtractionResult>;\n\n\tabstract generatePdf(\n\t\thtmlContent: string,\n\t\toptions?: PdfGenerationOptions,\n\t): Promise<Buffer>;\n\n\tabstract convertToPdf(\n\t\tfilePath: string,\n\t\toptions?: PdfConversionOptions,\n\t): Promise<Buffer>;\n\n\tabstract mergePdfs(pdfPaths: (string | Buffer)[]): Promise<Buffer>;\n\n\tabstract splitPdf(pdfPath: string | Buffer): Promise<Buffer[]>;\n}\n\n// ============================================================================\n// Web Search Interfaces\n// ============================================================================\n\nexport abstract class IWebSearchService extends Service {\n\tstatic override readonly serviceType = ServiceType.WEB_SEARCH;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Web search and content discovery capabilities\";\n\n\tabstract search(\n\t\tquery: string,\n\t\toptions?: SearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchNews(\n\t\tquery: string,\n\t\toptions?: NewsSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchImages(\n\t\tquery: string,\n\t\toptions?: ImageSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract searchVideos(\n\t\tquery: string,\n\t\toptions?: VideoSearchOptions,\n\t): Promise<SearchResponse>;\n\n\tabstract getSuggestions(query: string): Promise<string[]>;\n\n\tabstract getTrendingSearches(region?: string): Promise<string[]>;\n\n\tabstract getPageInfo(url: string): Promise<{\n\t\ttitle: string;\n\t\tdescription: string;\n\t\tcontent: string;\n\t\tmetadata: Record<string, string>;\n\t\timages: string[];\n\t\tlinks: string[];\n\t}>;\n}\n\n// ============================================================================\n// Email Interfaces\n// ============================================================================\n\nexport abstract class IEmailService extends Service {\n\tstatic override readonly serviceType = ServiceType.EMAIL;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Email sending, receiving, and management capabilities\";\n\n\tabstract sendEmail(\n\t\tmessage: EmailMessage,\n\t\toptions?: EmailSendOptions,\n\t): Promise<string>;\n\n\tabstract getEmails(options?: EmailSearchOptions): Promise<EmailMessage[]>;\n\n\tabstract getEmail(messageId: string): Promise<EmailMessage>;\n\n\tabstract deleteEmail(messageId: string): Promise<void>;\n\n\tabstract markEmailAsRead(messageId: string, read: boolean): Promise<void>;\n\n\tabstract flagEmail(messageId: string, flagged: boolean): Promise<void>;\n\n\tabstract moveEmail(messageId: string, folderPath: string): Promise<void>;\n\n\tabstract getFolders(): Promise<EmailFolder[]>;\n\n\tabstract createFolder(folderName: string, parentPath?: string): Promise<void>;\n\n\tabstract getAccountInfo(): Promise<EmailAccount>;\n\n\tabstract searchEmails(\n\t\tquery: string,\n\t\toptions?: EmailSearchOptions,\n\t): Promise<EmailMessage[]>;\n}\n\n// ============================================================================\n// Message Interfaces\n// ============================================================================\n\nexport abstract class IMessagingService extends Service {\n\tstatic override readonly serviceType = ServiceType.MESSAGE;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Platform messaging and channel management capabilities\";\n\n\tabstract sendMessage(\n\t\tchannelId: UUID,\n\t\tcontent: MessageContent,\n\t\toptions?: MessageSendOptions,\n\t): Promise<UUID>;\n\n\tabstract getMessages(\n\t\tchannelId: UUID,\n\t\toptions?: MessageSearchOptions,\n\t): Promise<MessageInfo[]>;\n\n\tabstract getMessage(messageId: UUID): Promise<MessageInfo>;\n\n\tabstract editMessage(messageId: UUID, content: MessageContent): Promise<void>;\n\n\tabstract deleteMessage(messageId: UUID): Promise<void>;\n\n\tabstract addReaction(messageId: UUID, emoji: string): Promise<void>;\n\n\tabstract removeReaction(messageId: UUID, emoji: string): Promise<void>;\n\n\tabstract pinMessage(messageId: UUID): Promise<void>;\n\n\tabstract unpinMessage(messageId: UUID): Promise<void>;\n\n\tabstract getChannels(): Promise<MessageChannel[]>;\n\n\tabstract getChannel(channelId: UUID): Promise<MessageChannel>;\n\n\tabstract createChannel(\n\t\tname: string,\n\t\ttype: MessageChannel[\"type\"],\n\t\toptions?: {\n\t\t\tdescription?: string;\n\t\t\tparticipants?: UUID[];\n\t\t\tprivate?: boolean;\n\t\t},\n\t): Promise<UUID>;\n\n\tabstract searchMessages(\n\t\tquery: string,\n\t\toptions?: MessageSearchOptions,\n\t): Promise<MessageInfo[]>;\n}\n\n// ============================================================================\n// Post/Social Media Interfaces\n// ============================================================================\n\nexport abstract class IPostService extends Service {\n\tstatic override readonly serviceType = ServiceType.POST;\n\n\tpublic readonly capabilityDescription =\n\t\t\"Social media posting and content management capabilities\";\n\n\tabstract createPost(\n\t\tcontent: PostContent,\n\t\toptions?: PostCreateOptions,\n\t): Promise<UUID>;\n\n\tabstract getPosts(options?: PostSearchOptions): Promise<PostInfo[]>;\n\n\tabstract getPost(postId: UUID): Promise<PostInfo>;\n\n\tabstract editPost(postId: UUID, content: PostContent): Promise<void>;\n\n\tabstract deletePost(postId: UUID): Promise<void>;\n\n\tabstract likePost(postId: UUID, like: boolean): Promise<void>;\n\n\tabstract sharePost(postId: UUID, comment?: string): Promise<UUID>;\n\n\tabstract savePost(postId: UUID, save: boolean): Promise<void>;\n\n\tabstract commentOnPost(postId: UUID, content: PostContent): Promise<UUID>;\n\n\tabstract getComments(\n\t\tpostId: UUID,\n\t\toptions?: PostSearchOptions,\n\t): Promise<PostInfo[]>;\n\n\tabstract schedulePost(\n\t\tcontent: PostContent,\n\t\tscheduledAt: Date,\n\t\toptions?: PostCreateOptions,\n\t): Promise<UUID>;\n\n\tabstract getPostAnalytics(postId: UUID): Promise<PostAnalytics>;\n\n\tabstract getTrendingPosts(options?: PostSearchOptions): Promise<PostInfo[]>;\n\n\tabstract searchPosts(\n\t\tquery: string,\n\t\toptions?: PostSearchOptions,\n\t): Promise<PostInfo[]>;\n}\n\n// ============================================================================\n// Transcription & Audio Interfaces\n// ============================================================================\n\n/**\n * Options for audio transcription.\n */\nexport interface TranscriptionOptions {\n\t/** Language code for transcription */\n\tlanguage?: string;\n\t/** Model to use for transcription */\n\tmodel?: string;\n\t/** Temperature for generation */\n\ttemperature?: number;\n\t/** Prompt to guide transcription */\n\tprompt?: string;\n\t/** Response format */\n\tresponse_format?: \"json\" | \"text\" | \"srt\" | \"vtt\" | \"verbose_json\";\n\t/** Timestamp granularities to include */\n\ttimestamp_granularities?: (\"word\" | \"segment\")[];\n\t/** Include word-level timestamps */\n\tword_timestamps?: boolean;\n\t/** Include segment-level timestamps */\n\tsegment_timestamps?: boolean;\n}\n\n/**\n * Result of audio transcription.\n */\nexport interface TranscriptionResult {\n\t/** Transcribed text */\n\ttext: string;\n\t/** Detected language */\n\tlanguage?: string;\n\t/** Audio duration in seconds */\n\tduration?: number;\n\t/** Transcription segments */\n\tsegments?: TranscriptionSegment[];\n\t/** Word-level transcription */\n\twords?: TranscriptionWord[];\n\t/** Overall confidence score */\n\tconfidence?: number;\n}\n\n/**\n * A segment of transcription.\n */\nexport interface TranscriptionSegment {\n\t/** Segment ID */\n\tid: number;\n\t/** Segment text */\n\ttext: string;\n\t/** Start time in seconds */\n\tstart: number;\n\t/** End time in seconds */\n\tend: number;\n\t/** Confidence score */\n\tconfidence?: number;\n\t/** Token IDs */\n\ttokens?: number[];\n\t/** Temperature used */\n\ttemperature?: number;\n\t/** Average log probability */\n\tavg_logprob?: number;\n\t/** Compression ratio */\n\tcompression_ratio?: number;\n\t/** No speech probability */\n\tno_speech_prob?: number;\n}\n\n/**\n * A word in transcription.\n */\nexport interface TranscriptionWord {\n\t/** The word */\n\tword: string;\n\t/** Start time in seconds */\n\tstart: number;\n\t/** End time in seconds */\n\tend: number;\n\t/** Confidence score */\n\tconfidence?: number;\n}\n\n/**\n * Options for speech-to-text.\n */\nexport interface SpeechToTextOptions {\n\t/** Language code */\n\tlanguage?: string;\n\t/** Model to use */\n\tmodel?: string;\n\t/** Enable continuous recognition */\n\tcontinuous?: boolean;\n\t/** Return interim results */\n\tinterimResults?: boolean;\n\t/** Maximum alternatives to return */\n\tmaxAlternatives?: number;\n}\n\n/**\n * Options for text-to-speech.\n */\nexport interface TextToSpeechOptions {\n\t/** Voice to use */\n\tvoice?: string;\n\t/** Model to use */\n\tmodel?: string;\n\t/** Speech speed */\n\tspeed?: number;\n\t/** Output format */\n\tformat?: \"mp3\" | \"wav\" | \"flac\" | \"aac\";\n\t/** Response format */\n\tresponse_format?: \"mp3\" | \"opus\" | \"aac\" | \"flac\";\n}\n\n// ============================================================================\n// Video Interfaces\n// ============================================================================\n\n/**\n * Video information.\n */\nexport interface VideoInfo {\n\t/** Video title */\n\ttitle?: string;\n\t/** Duration in seconds */\n\tduration?: number;\n\t/** Video URL */\n\turl: string;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Video description */\n\tdescription?: string;\n\t/** Uploader name */\n\tuploader?: string;\n\t/** View count */\n\tviewCount?: number;\n\t/** Upload date */\n\tuploadDate?: Date;\n\t/** Available formats */\n\tformats?: VideoFormat[];\n}\n\n/**\n * Video format information.\n */\nexport interface VideoFormat {\n\t/** Format ID */\n\tformatId: string;\n\t/** Download URL */\n\turl: string;\n\t/** File extension */\n\textension: string;\n\t/** Quality label */\n\tquality: string;\n\t/** File size in bytes */\n\tfileSize?: number;\n\t/** Video codec */\n\tvideoCodec?: string;\n\t/** Audio codec */\n\taudioCodec?: string;\n\t/** Resolution (e.g., \"1920x1080\") */\n\tresolution?: string;\n\t/** Frames per second */\n\tfps?: number;\n\t/** Bitrate */\n\tbitrate?: number;\n}\n\n/**\n * Video download options.\n */\nexport interface VideoDownloadOptions {\n\t/** Preferred format */\n\tformat?: string;\n\t/** Quality preference */\n\tquality?: \"best\" | \"worst\" | \"bestvideo\" | \"bestaudio\" | string;\n\t/** Output file path */\n\toutputPath?: string;\n\t/** Extract audio only */\n\taudioOnly?: boolean;\n\t/** Extract video only (no audio) */\n\tvideoOnly?: boolean;\n\t/** Download subtitles */\n\tsubtitles?: boolean;\n\t/** Embed subtitles in video */\n\tembedSubs?: boolean;\n\t/** Write info JSON file */\n\twriteInfoJson?: boolean;\n}\n\n/**\n * Video processing options.\n */\nexport interface VideoProcessingOptions {\n\t/** Start time in seconds */\n\tstartTime?: number;\n\t/** End time in seconds */\n\tendTime?: number;\n\t/** Output format */\n\toutputFormat?: string;\n\t/** Target resolution */\n\tresolution?: string;\n\t/** Target bitrate */\n\tbitrate?: string;\n\t/** Target framerate */\n\tframerate?: number;\n\t/** Audio codec */\n\taudioCodec?: string;\n\t/** Video codec */\n\tvideoCodec?: string;\n}\n\n// ============================================================================\n// Browser Interfaces\n// ============================================================================\n\n/**\n * Browser navigation options.\n */\nexport interface BrowserNavigationOptions {\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Wait until condition */\n\twaitUntil?: \"load\" | \"domcontentloaded\" | \"networkidle0\" | \"networkidle2\";\n\t/** Viewport size */\n\tviewport?: {\n\t\twidth: number;\n\t\theight: number;\n\t};\n\t/** User agent string */\n\tuserAgent?: string;\n\t/** Additional headers */\n\theaders?: Record<string, string>;\n}\n\n/**\n * Screenshot options.\n */\nexport interface ScreenshotOptions {\n\t/** Capture full page */\n\tfullPage?: boolean;\n\t/** Clip region */\n\tclip?: {\n\t\tx: number;\n\t\ty: number;\n\t\twidth: number;\n\t\theight: number;\n\t};\n\t/** Image format */\n\tformat?: \"png\" | \"jpeg\" | \"webp\";\n\t/** Image quality (0-100) */\n\tquality?: number;\n\t/** Omit background */\n\tomitBackground?: boolean;\n}\n\n/**\n * Element selector options.\n */\nexport interface ElementSelector {\n\t/** CSS selector */\n\tselector: string;\n\t/** Text content to match */\n\ttext?: string;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n}\n\n/**\n * Extracted content from a page.\n */\nexport interface ExtractedContent {\n\t/** Text content */\n\ttext: string;\n\t/** HTML content */\n\thtml: string;\n\t/** Links found on page */\n\tlinks: Array<{\n\t\turl: string;\n\t\ttext: string;\n\t}>;\n\t/** Images found on page */\n\timages: Array<{\n\t\tsrc: string;\n\t\talt?: string;\n\t}>;\n\t/** Page title */\n\ttitle?: string;\n\t/** Page metadata */\n\tmetadata?: Record<string, string>;\n}\n\n/**\n * Click options.\n */\nexport interface ClickOptions {\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Force click even if element is obscured */\n\tforce?: boolean;\n\t/** Wait for navigation after click */\n\twaitForNavigation?: boolean;\n}\n\n/**\n * Type/input options.\n */\nexport interface TypeOptions {\n\t/** Delay between keystrokes */\n\tdelay?: number;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Clear field before typing */\n\tclear?: boolean;\n}\n\n// ============================================================================\n// PDF Interfaces\n// ============================================================================\n\n/**\n * PDF text extraction result.\n */\nexport interface PdfExtractionResult {\n\t/** Extracted text */\n\ttext: string;\n\t/** Total page count */\n\tpageCount: number;\n\t/** PDF metadata */\n\tmetadata?: {\n\t\ttitle?: string;\n\t\tauthor?: string;\n\t\tcreatedAt?: Date;\n\t\tmodifiedAt?: Date;\n\t};\n}\n\n/**\n * PDF generation options.\n */\nexport interface PdfGenerationOptions {\n\t/** Paper format */\n\tformat?: \"A4\" | \"A3\" | \"Letter\";\n\t/** Page orientation */\n\torientation?: \"portrait\" | \"landscape\";\n\t/** Page margins */\n\tmargins?: {\n\t\ttop?: number;\n\t\tbottom?: number;\n\t\tleft?: number;\n\t\tright?: number;\n\t};\n\t/** Header content */\n\theader?: string;\n\t/** Footer content */\n\tfooter?: string;\n}\n\n/**\n * PDF conversion options.\n */\nexport interface PdfConversionOptions {\n\t/** Output quality */\n\tquality?: \"high\" | \"medium\" | \"low\";\n\t/** Output format */\n\toutputFormat?: \"pdf\" | \"pdf/a\";\n\t/** Enable compression */\n\tcompression?: boolean;\n}\n\n// ============================================================================\n// Web Search Interfaces\n// ============================================================================\n\n/**\n * Web search options.\n */\nexport interface SearchOptions {\n\t/** Maximum results to return */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Language code */\n\tlanguage?: string;\n\t/** Region code */\n\tregion?: string;\n\t/** Date range filter */\n\tdateRange?: {\n\t\tstart?: Date;\n\t\tend?: Date;\n\t};\n\t/** File type filter */\n\tfileType?: string;\n\t/** Limit to specific site */\n\tsite?: string;\n\t/** Sort order */\n\tsortBy?: \"relevance\" | \"date\" | \"popularity\";\n\t/** Safe search level */\n\tsafeSearch?: \"strict\" | \"moderate\" | \"off\";\n}\n\n/**\n * A single search result.\n */\nexport interface SearchResult {\n\t/** Result title */\n\ttitle: string;\n\t/** Result URL */\n\turl: string;\n\t/** Result description/snippet */\n\tdescription: string;\n\t/** Display URL */\n\tdisplayUrl?: string;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Published date */\n\tpublishedDate?: Date;\n\t/** Source name */\n\tsource?: string;\n\t/** Relevance score */\n\trelevanceScore?: number;\n\t/** Text snippet */\n\tsnippet?: string;\n}\n\n/**\n * Search response containing results.\n */\nexport interface SearchResponse {\n\t/** Original query */\n\tquery: string;\n\t/** Search results */\n\tresults: SearchResult[];\n\t/** Total available results */\n\ttotalResults?: number;\n\t/** Search time in seconds */\n\tsearchTime?: number;\n\t/** Query suggestions */\n\tsuggestions?: string[];\n\t/** Token for next page */\n\tnextPageToken?: string;\n\t/** Related search queries */\n\trelatedSearches?: string[];\n}\n\n/**\n * News search options.\n */\nexport interface NewsSearchOptions extends SearchOptions {\n\t/** News category */\n\tcategory?:\n\t\t| \"general\"\n\t\t| \"business\"\n\t\t| \"entertainment\"\n\t\t| \"health\"\n\t\t| \"science\"\n\t\t| \"sports\"\n\t\t| \"technology\";\n\t/** Freshness filter */\n\tfreshness?: \"day\" | \"week\" | \"month\";\n}\n\n/**\n * Image search options.\n */\nexport interface ImageSearchOptions extends SearchOptions {\n\t/** Image size filter */\n\tsize?: \"small\" | \"medium\" | \"large\" | \"wallpaper\" | \"any\";\n\t/** Color filter */\n\tcolor?:\n\t\t| \"color\"\n\t\t| \"monochrome\"\n\t\t| \"red\"\n\t\t| \"orange\"\n\t\t| \"yellow\"\n\t\t| \"green\"\n\t\t| \"blue\"\n\t\t| \"purple\"\n\t\t| \"pink\"\n\t\t| \"brown\"\n\t\t| \"black\"\n\t\t| \"gray\"\n\t\t| \"white\";\n\t/** Image type filter */\n\ttype?: \"photo\" | \"clipart\" | \"line\" | \"animated\";\n\t/** Image layout filter */\n\tlayout?: \"square\" | \"wide\" | \"tall\" | \"any\";\n\t/** License filter */\n\tlicense?: \"any\" | \"public\" | \"share\" | \"sharecommercially\" | \"modify\";\n}\n\n/**\n * Video search options.\n */\nexport interface VideoSearchOptions extends SearchOptions {\n\t/** Duration filter */\n\tduration?: \"short\" | \"medium\" | \"long\" | \"any\";\n\t/** Resolution filter */\n\tresolution?: \"high\" | \"standard\" | \"any\";\n\t/** Quality filter */\n\tquality?: \"high\" | \"standard\" | \"any\";\n}\n\n// ============================================================================\n// Email Interfaces\n// ============================================================================\n\n/**\n * Email address with optional name.\n */\nexport interface EmailAddress {\n\t/** Email address */\n\temail: string;\n\t/** Display name */\n\tname?: string;\n}\n\n/**\n * Email attachment.\n */\nexport interface EmailAttachment {\n\t/** Filename */\n\tfilename: string;\n\t/** Content as buffer or base64 string */\n\tcontent: Buffer | string;\n\t/** MIME type */\n\tcontentType?: string;\n\t/** Content disposition */\n\tcontentDisposition?: \"attachment\" | \"inline\";\n\t/** Content ID for inline attachments */\n\tcid?: string;\n}\n\n/**\n * Email message.\n */\nexport interface EmailMessage {\n\t/** Sender address */\n\tfrom: EmailAddress;\n\t/** Recipients */\n\tto: EmailAddress[];\n\t/** CC recipients */\n\tcc?: EmailAddress[];\n\t/** BCC recipients */\n\tbcc?: EmailAddress[];\n\t/** Email subject */\n\tsubject: string;\n\t/** Plain text body */\n\ttext?: string;\n\t/** HTML body */\n\thtml?: string;\n\t/** Attachments */\n\tattachments?: EmailAttachment[];\n\t/** Reply-to address */\n\treplyTo?: EmailAddress;\n\t/** Send date */\n\tdate?: Date;\n\t/** Message ID */\n\tmessageId?: string;\n\t/** References header */\n\treferences?: string[];\n\t/** In-Reply-To header */\n\tinReplyTo?: string;\n\t/** Priority level */\n\tpriority?: \"high\" | \"normal\" | \"low\";\n}\n\n/**\n * Email send options.\n */\nexport interface EmailSendOptions {\n\t/** Number of retries */\n\tretry?: number;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Track email opens */\n\ttrackOpens?: boolean;\n\t/** Track link clicks */\n\ttrackClicks?: boolean;\n\t/** Tags for categorization */\n\ttags?: string[];\n}\n\n/**\n * Email search options.\n */\nexport interface EmailSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by sender */\n\tfrom?: string;\n\t/** Filter by recipient */\n\tto?: string;\n\t/** Filter by subject */\n\tsubject?: string;\n\t/** Filter by folder */\n\tfolder?: string;\n\t/** Filter emails since date */\n\tsince?: Date;\n\t/** Filter emails before date */\n\tbefore?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter unread only */\n\tunread?: boolean;\n\t/** Filter flagged only */\n\tflagged?: boolean;\n\t/** Filter with attachments only */\n\thasAttachments?: boolean;\n}\n\n/**\n * Email folder.\n */\nexport interface EmailFolder {\n\t/** Folder name */\n\tname: string;\n\t/** Folder path */\n\tpath: string;\n\t/** Folder type */\n\ttype: \"inbox\" | \"sent\" | \"drafts\" | \"trash\" | \"spam\" | \"custom\";\n\t/** Total message count */\n\tmessageCount?: number;\n\t/** Unread message count */\n\tunreadCount?: number;\n\t/** Child folders */\n\tchildren?: EmailFolder[];\n}\n\n/**\n * Email account information.\n */\nexport interface EmailAccount {\n\t/** Email address */\n\temail: string;\n\t/** Display name */\n\tname?: string;\n\t/** Email provider */\n\tprovider?: string;\n\t/** Available folders */\n\tfolders?: EmailFolder[];\n\t/** Storage used in bytes */\n\tquotaUsed?: number;\n\t/** Storage limit in bytes */\n\tquotaLimit?: number;\n}\n\n// ============================================================================\n// Message Interfaces\n// ============================================================================\n\n/**\n * Message participant information.\n */\nexport interface MessageParticipant {\n\t/** Participant ID */\n\tid: UUID;\n\t/** Display name */\n\tname: string;\n\t/** Username */\n\tusername?: string;\n\t/** Avatar URL */\n\tavatar?: string;\n\t/** Online status */\n\tstatus?: \"online\" | \"offline\" | \"away\" | \"busy\";\n}\n\n/**\n * Message attachment.\n */\nexport interface MessageAttachment {\n\t/** Attachment ID */\n\tid: UUID;\n\t/** Filename */\n\tfilename: string;\n\t/** File URL */\n\turl: string;\n\t/** MIME type */\n\tmimeType: string;\n\t/** File size in bytes */\n\tsize: number;\n\t/** Width for images/videos */\n\twidth?: number;\n\t/** Height for images/videos */\n\theight?: number;\n\t/** Duration for audio/video */\n\tduration?: number;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n}\n\n/**\n * Message reaction.\n */\nexport interface MessageReaction {\n\t/** Emoji used */\n\temoji: string;\n\t/** Number of reactions */\n\tcount: number;\n\t/** User IDs who reacted */\n\tusers: UUID[];\n\t/** Whether current user has reacted */\n\thasReacted: boolean;\n}\n\n/**\n * Message reference (reply/forward/quote).\n */\nexport interface MessageReference {\n\t/** Referenced message ID */\n\tmessageId: UUID;\n\t/** Channel of referenced message */\n\tchannelId: UUID;\n\t/** Type of reference */\n\ttype: \"reply\" | \"forward\" | \"quote\";\n}\n\n/**\n * Message content.\n */\nexport interface MessageContent {\n\t/** Plain text content */\n\ttext?: string;\n\t/** HTML content */\n\thtml?: string;\n\t/** Markdown content */\n\tmarkdown?: string;\n\t/** Attachments */\n\tattachments?: MessageAttachment[];\n\t/** Reactions */\n\treactions?: MessageReaction[];\n\t/** Reference to another message */\n\treference?: MessageReference;\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Embedded content */\n\tembeds?: Array<{\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\turl?: string;\n\t\timage?: string;\n\t\tfields?: Array<{\n\t\t\tname: string;\n\t\t\tvalue: string;\n\t\t\tinline?: boolean;\n\t\t}>;\n\t}>;\n}\n\n/**\n * Message information.\n */\nexport interface MessageInfo {\n\t/** Message ID */\n\tid: UUID;\n\t/** Channel ID */\n\tchannelId: UUID;\n\t/** Sender ID */\n\tsenderId: UUID;\n\t/** Message content */\n\tcontent: MessageContent;\n\t/** Sent timestamp */\n\ttimestamp: Date;\n\t/** Edit timestamp */\n\tedited?: Date;\n\t/** Deletion timestamp */\n\tdeleted?: Date;\n\t/** Whether message is pinned */\n\tpinned?: boolean;\n\t/** Thread information */\n\tthread?: {\n\t\tid: UUID;\n\t\tmessageCount: number;\n\t\tparticipants: UUID[];\n\t\tlastMessageAt: Date;\n\t};\n}\n\n/**\n * Message send options.\n */\nexport interface MessageSendOptions {\n\t/** Reply to message ID */\n\treplyTo?: UUID;\n\t/** Ephemeral (only visible to sender) */\n\tephemeral?: boolean;\n\t/** Silent (no notification) */\n\tsilent?: boolean;\n\t/** Scheduled send time */\n\tscheduled?: Date;\n\t/** Thread ID */\n\tthread?: UUID;\n\t/** Nonce for deduplication */\n\tnonce?: string;\n}\n\n/**\n * Message search options.\n */\nexport interface MessageSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by channel */\n\tchannelId?: UUID;\n\t/** Filter by sender */\n\tsenderId?: UUID;\n\t/** Filter messages before date */\n\tbefore?: Date;\n\t/** Filter messages after date */\n\tafter?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter with attachments only */\n\thasAttachments?: boolean;\n\t/** Filter pinned only */\n\tpinned?: boolean;\n\t/** Filter mentioning user */\n\tmentions?: UUID;\n}\n\n/**\n * Message channel.\n */\nexport interface MessageChannel {\n\t/** Channel ID */\n\tid: UUID;\n\t/** Channel name */\n\tname: string;\n\t/** Channel type */\n\ttype: \"text\" | \"voice\" | \"dm\" | \"group\" | \"announcement\" | \"thread\";\n\t/** Channel description */\n\tdescription?: string;\n\t/** Channel participants */\n\tparticipants?: MessageParticipant[];\n\t/** User permissions */\n\tpermissions?: {\n\t\tcanSend: boolean;\n\t\tcanRead: boolean;\n\t\tcanDelete: boolean;\n\t\tcanPin: boolean;\n\t\tcanManage: boolean;\n\t};\n\t/** Last message timestamp */\n\tlastMessageAt?: Date;\n\t/** Total message count */\n\tmessageCount?: number;\n\t/** Unread message count */\n\tunreadCount?: number;\n}\n\n// ============================================================================\n// Post/Social Media Interfaces\n// ============================================================================\n\n/**\n * Post media content.\n */\nexport interface PostMedia {\n\t/** Media ID */\n\tid: UUID;\n\t/** Media URL */\n\turl: string;\n\t/** Media type */\n\ttype: \"image\" | \"video\" | \"audio\" | \"document\";\n\t/** MIME type */\n\tmimeType: string;\n\t/** File size in bytes */\n\tsize: number;\n\t/** Width for images/videos */\n\twidth?: number;\n\t/** Height for images/videos */\n\theight?: number;\n\t/** Duration for audio/video */\n\tduration?: number;\n\t/** Thumbnail URL */\n\tthumbnail?: string;\n\t/** Description */\n\tdescription?: string;\n\t/** Alt text for accessibility */\n\taltText?: string;\n}\n\n/**\n * Post location.\n */\nexport interface PostLocation {\n\t/** Location name */\n\tname: string;\n\t/** Address */\n\taddress?: string;\n\t/** Coordinates */\n\tcoordinates?: {\n\t\tlatitude: number;\n\t\tlongitude: number;\n\t};\n\t/** Place ID from location service */\n\tplaceId?: string;\n}\n\n/**\n * Post author information.\n */\nexport interface PostAuthor {\n\t/** Author ID */\n\tid: UUID;\n\t/** Username */\n\tusername: string;\n\t/** Display name */\n\tdisplayName: string;\n\t/** Avatar URL */\n\tavatar?: string;\n\t/** Verified badge */\n\tverified?: boolean;\n\t/** Follower count */\n\tfollowerCount?: number;\n\t/** Following count */\n\tfollowingCount?: number;\n\t/** Bio */\n\tbio?: string;\n\t/** Website URL */\n\twebsite?: string;\n}\n\n/**\n * Post engagement metrics.\n */\nexport interface PostEngagement {\n\t/** Number of likes */\n\tlikes: number;\n\t/** Number of shares */\n\tshares: number;\n\t/** Number of comments */\n\tcomments: number;\n\t/** Number of views */\n\tviews?: number;\n\t/** Whether current user has liked */\n\thasLiked: boolean;\n\t/** Whether current user has shared */\n\thasShared: boolean;\n\t/** Whether current user has commented */\n\thasCommented: boolean;\n\t/** Whether current user has saved */\n\thasSaved: boolean;\n}\n\n/**\n * Post content.\n */\nexport interface PostContent {\n\t/** Text content */\n\ttext?: string;\n\t/** HTML content */\n\thtml?: string;\n\t/** Media attachments */\n\tmedia?: PostMedia[];\n\t/** Location */\n\tlocation?: PostLocation;\n\t/** Hashtags */\n\ttags?: string[];\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Link previews */\n\tlinks?: Array<{\n\t\turl: string;\n\t\ttitle?: string;\n\t\tdescription?: string;\n\t\timage?: string;\n\t}>;\n\t/** Poll */\n\tpoll?: {\n\t\tquestion: string;\n\t\toptions: Array<{\n\t\t\ttext: string;\n\t\t\tvotes: number;\n\t\t}>;\n\t\texpiresAt?: Date;\n\t\tmultipleChoice?: boolean;\n\t};\n}\n\n/**\n * Post information.\n */\nexport interface PostInfo {\n\t/** Post ID */\n\tid: UUID;\n\t/** Post author */\n\tauthor: PostAuthor;\n\t/** Post content */\n\tcontent: PostContent;\n\t/** Platform name */\n\tplatform: string;\n\t/** Platform-specific ID */\n\tplatformId: string;\n\t/** Post URL */\n\turl: string;\n\t/** Created timestamp */\n\tcreatedAt: Date;\n\t/** Edited timestamp */\n\teditedAt?: Date;\n\t/** Scheduled timestamp */\n\tscheduledAt?: Date;\n\t/** Engagement metrics */\n\tengagement: PostEngagement;\n\t/** Visibility level */\n\tvisibility: \"public\" | \"private\" | \"followers\" | \"friends\" | \"unlisted\";\n\t/** Reply to post ID */\n\treplyTo?: UUID;\n\t/** Thread information */\n\tthread?: {\n\t\tid: UUID;\n\t\tposition: number;\n\t\ttotal: number;\n\t};\n\t/** Cross-post information */\n\tcrossPosted?: Array<{\n\t\tplatform: string;\n\t\tplatformId: string;\n\t\turl: string;\n\t}>;\n}\n\n/**\n * Post creation options.\n */\nexport interface PostCreateOptions {\n\t/** Target platforms */\n\tplatforms?: string[];\n\t/** Scheduled time */\n\tscheduledAt?: Date;\n\t/** Visibility level */\n\tvisibility?: PostInfo[\"visibility\"];\n\t/** Reply to post ID */\n\treplyTo?: UUID;\n\t/** Create as thread */\n\tthread?: boolean;\n\t/** Location */\n\tlocation?: PostLocation;\n\t/** Hashtags */\n\ttags?: string[];\n\t/** Mentioned user IDs */\n\tmentions?: UUID[];\n\t/** Enable comments */\n\tenableComments?: boolean;\n\t/** Enable sharing */\n\tenableSharing?: boolean;\n\t/** Content warning */\n\tcontentWarning?: string;\n\t/** Mark as sensitive */\n\tsensitive?: boolean;\n}\n\n/**\n * Post search options.\n */\nexport interface PostSearchOptions {\n\t/** Search query */\n\tquery?: string;\n\t/** Filter by author */\n\tauthor?: UUID;\n\t/** Filter by platform */\n\tplatform?: string;\n\t/** Filter by tags */\n\ttags?: string[];\n\t/** Filter by mentions */\n\tmentions?: UUID[];\n\t/** Filter posts since date */\n\tsince?: Date;\n\t/** Filter posts before date */\n\tbefore?: Date;\n\t/** Maximum results */\n\tlimit?: number;\n\t/** Offset for pagination */\n\toffset?: number;\n\t/** Filter with media only */\n\thasMedia?: boolean;\n\t/** Filter with location only */\n\thasLocation?: boolean;\n\t/** Filter by visibility */\n\tvisibility?: PostInfo[\"visibility\"];\n\t/** Sort order */\n\tsortBy?: \"date\" | \"engagement\" | \"relevance\";\n}\n\n/**\n * Post analytics.\n */\nexport interface PostAnalytics {\n\t/** Post ID */\n\tpostId: UUID;\n\t/** Platform name */\n\tplatform: string;\n\t/** Total impressions */\n\timpressions: number;\n\t/** Unique reach */\n\treach: number;\n\t/** Engagement metrics */\n\tengagement: PostEngagement;\n\t/** Link clicks */\n\tclicks: number;\n\t/** Shares */\n\tshares: number;\n\t/** Saves */\n\tsaves: number;\n\t/** Demographics */\n\tdemographics?: {\n\t\tage?: Record<string, number>;\n\t\tgender?: Record<string, number>;\n\t\tlocation?: Record<string, number>;\n\t};\n\t/** Top performing hours */\n\ttopPerformingHours?: Array<{\n\t\thour: number;\n\t\tengagement: number;\n\t}>;\n}\n",
|
|
459
459
|
"/**\n * Tool Policy Types\n *\n * Types and definitions for tool/action filtering and permissions in elizaOS.\n *\n * @module tools\n */\n\n// Re-export from channel-config to avoid duplication\nexport type { ToolPolicyConfig, ToolProfileId } from \"./channel-config\";\n\nimport type { ToolPolicyConfig, ToolProfileId } from \"./channel-config\";\n\n/**\n * Canonical tool name aliases for backward compatibility.\n * Maps legacy names to canonical names.\n */\nexport const TOOL_NAME_ALIASES: Record<string, string> = {\n\tbash: \"exec\",\n\t\"apply-patch\": \"apply_patch\",\n};\n\n/**\n * Predefined tool groups for easier policy configuration.\n * Use \"group:<name>\" syntax in policy configs (e.g., \"group:fs\").\n */\nexport const TOOL_GROUPS: Record<string, string[]> = {\n\t// Memory tools (provided by plugin-scratchpad)\n\t\"group:memory\": [\n\t\t\"scratchpad_search\",\n\t\t\"scratchpad_read\",\n\t\t\"read_attachment\",\n\t\t\"remove_from_scratchpad\",\n\t],\n\t// Web tools\n\t\"group:web\": [\"web_search\", \"web_fetch\"],\n\t// Basic workspace/file tools\n\t\"group:fs\": [\"read\", \"read_file\", \"write\", \"edit\", \"apply_patch\"],\n\t// Host/runtime execution tools\n\t\"group:runtime\": [\"exec\", \"process\"],\n\t// Session management tools\n\t\"group:sessions\": [\n\t\t\"sessions_list\",\n\t\t\"sessions_history\",\n\t\t\"sessions_send\",\n\t\t\"sessions_spawn\",\n\t\t\"session_status\",\n\t],\n\t// UI helpers\n\t\"group:ui\": [\"browser\", \"canvas\"],\n\t// Automation + infra\n\t\"group:automation\": [\"cron\", \"gateway\"],\n\t// Messaging surface\n\t\"group:messaging\": [\"message\"],\n\t// Nodes + device tools\n\t\"group:nodes\": [\"nodes\"],\n\t// All native tools (excludes provider plugins)\n\t\"group:all\": [\n\t\t\"browser\",\n\t\t\"canvas\",\n\t\t\"nodes\",\n\t\t\"cron\",\n\t\t\"message\",\n\t\t\"gateway\",\n\t\t\"agents_list\",\n\t\t\"sessions_list\",\n\t\t\"sessions_history\",\n\t\t\"sessions_send\",\n\t\t\"sessions_spawn\",\n\t\t\"session_status\",\n\t\t\"scratchpad_search\",\n\t\t\"scratchpad_read\",\n\t\t\"read_attachment\",\n\t\t\"read_file\",\n\t\t\"remove_from_scratchpad\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"image\",\n\t\t\"read\",\n\t\t\"write\",\n\t\t\"edit\",\n\t\t\"apply_patch\",\n\t\t\"exec\",\n\t\t\"process\",\n\t],\n};\n\n/**\n * Predefined tool profiles with default allow/deny policies.\n */\nexport const TOOL_PROFILES: Record<ToolProfileId, ToolPolicyConfig> = {\n\tminimal: {\n\t\tallow: [\"session_status\"],\n\t},\n\tcoding: {\n\t\tallow: [\n\t\t\t\"group:fs\",\n\t\t\t\"group:runtime\",\n\t\t\t\"group:sessions\",\n\t\t\t\"group:memory\",\n\t\t\t\"image\",\n\t\t],\n\t},\n\tmessaging: {\n\t\tallow: [\n\t\t\t\"group:messaging\",\n\t\t\t\"sessions_list\",\n\t\t\t\"sessions_history\",\n\t\t\t\"sessions_send\",\n\t\t\t\"session_status\",\n\t\t],\n\t},\n\tfull: {\n\t\t// No restrictions - all tools allowed\n\t},\n};\n\n/**\n * Plugin tool groups for dynamic tool resolution.\n */\nexport interface PluginToolGroups {\n\t/** All tool names from plugins */\n\tall: string[];\n\t/** Tool names organized by plugin ID */\n\tbyPlugin: Map<string, string[]>;\n}\n\n/**\n * Result of allowlist resolution with diagnostics.\n */\nexport interface AllowlistResolution {\n\t/** The resolved policy after processing */\n\tpolicy: ToolPolicyConfig | undefined;\n\t/** Entries in the allowlist that weren't recognized */\n\tunknownAllowlist: string[];\n\t/** Whether the allowlist was stripped (contained only plugin tools) */\n\tstrippedAllowlist: boolean;\n}\n\n/**\n * Tool policy evaluation options.\n */\nexport interface ToolPolicyEvaluationOptions {\n\t/** The character's tool profile */\n\tprofile?: ToolProfileId;\n\t/** Character-level tool policy overrides */\n\tcharacterPolicy?: ToolPolicyConfig;\n\t/** Channel-specific tool policy overrides */\n\tchannelPolicy?: ToolPolicyConfig;\n\t/** Provider-specific tool policy overrides */\n\tproviderPolicy?: ToolPolicyConfig;\n\t/** Plugin tool groups for resolution */\n\tpluginGroups?: PluginToolGroups;\n\t/** Set of core tool names for validation */\n\tcoreTools?: Set<string>;\n}\n\n/**\n * Tool policy evaluation result.\n */\nexport interface ToolPolicyResult {\n\t/** Whether the tool is allowed */\n\tallowed: boolean;\n\t/** Reason for the decision */\n\treason: string;\n\t/** The effective policy after merging */\n\teffectivePolicy: ToolPolicyConfig;\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Normalize a tool name to its canonical form.\n * Handles aliases and case normalization.\n *\n * @param name - The tool name to normalize\n * @returns The canonical tool name\n */\nexport function normalizeToolName(name: string): string {\n\tconst normalized = name.trim().toLowerCase();\n\treturn TOOL_NAME_ALIASES[normalized] ?? normalized;\n}\n\n/**\n * Normalize a list of tool names.\n *\n * @param list - The list of tool names to normalize\n * @returns Normalized list with empty entries filtered out\n */\nexport function normalizeToolList(list?: string[]): string[] {\n\tif (!list) {\n\t\treturn [];\n\t}\n\treturn list.map(normalizeToolName).filter(Boolean);\n}\n\n/**\n * Expand tool groups in a list to their constituent tools.\n * Handles both group references (e.g., \"group:fs\") and individual tools.\n *\n * @param list - The list containing tool names and/or group references\n * @returns Expanded list with all groups resolved to individual tools\n */\nexport function expandToolGroups(list?: string[]): string[] {\n\tconst normalized = normalizeToolList(list);\n\tconst expanded: string[] = [];\n\n\tfor (const value of normalized) {\n\t\tconst group = TOOL_GROUPS[value];\n\t\tif (group) {\n\t\t\texpanded.push(...group);\n\t\t\tcontinue;\n\t\t}\n\t\texpanded.push(value);\n\t}\n\n\treturn Array.from(new Set(expanded));\n}\n\n/**\n * Resolve a tool profile to its policy configuration.\n *\n * @param profile - The profile ID to resolve\n * @returns The policy configuration, or undefined if profile is invalid\n */\nexport function resolveToolProfilePolicy(\n\tprofile?: string,\n): ToolPolicyConfig | undefined {\n\tif (!profile) {\n\t\treturn undefined;\n\t}\n\tconst resolved = TOOL_PROFILES[profile as ToolProfileId];\n\tif (!resolved) {\n\t\treturn undefined;\n\t}\n\t// Return undefined for 'full' profile (no restrictions)\n\tif (!resolved.allow && !resolved.deny) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tallow: resolved.allow ? [...resolved.allow] : undefined,\n\t\tdeny: resolved.deny ? [...resolved.deny] : undefined,\n\t};\n}\n\n/**\n * Collect all explicit allow entries from multiple policies.\n *\n * @param policies - Array of policies to collect from\n * @returns Combined allowlist entries\n */\nexport function collectExplicitAllowlist(\n\tpolicies: Array<ToolPolicyConfig | undefined>,\n): string[] {\n\tconst entries: string[] = [];\n\n\tfor (const policy of policies) {\n\t\tif (!policy?.allow) {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const value of policy.allow) {\n\t\t\tif (typeof value !== \"string\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst trimmed = value.trim();\n\t\t\tif (trimmed) {\n\t\t\t\tentries.push(trimmed);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Build plugin tool groups from a list of tools with metadata.\n *\n * @param params - Tools and metadata accessor\n * @returns Plugin tool groups organized by plugin ID\n */\nexport function buildPluginToolGroups<T extends { name: string }>(params: {\n\ttools: T[];\n\ttoolMeta: (tool: T) => { pluginId: string } | undefined;\n}): PluginToolGroups {\n\tconst all: string[] = [];\n\tconst byPlugin = new Map<string, string[]>();\n\n\tfor (const tool of params.tools) {\n\t\tconst meta = params.toolMeta(tool);\n\t\tif (!meta) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst name = normalizeToolName(tool.name);\n\t\tall.push(name);\n\t\tconst pluginId = meta.pluginId.toLowerCase();\n\t\tconst list = byPlugin.get(pluginId) ?? [];\n\t\tlist.push(name);\n\t\tbyPlugin.set(pluginId, list);\n\t}\n\n\treturn { all, byPlugin };\n}\n\n/**\n * Expand plugin group references in a list.\n *\n * @param list - The list containing potential plugin group references\n * @param groups - Plugin tool groups for resolution\n * @returns Expanded list with plugin groups resolved\n */\nexport function expandPluginGroups(\n\tlist: string[] | undefined,\n\tgroups: PluginToolGroups,\n): string[] | undefined {\n\tif (!list || list.length === 0) {\n\t\treturn list;\n\t}\n\n\tconst expanded: string[] = [];\n\tfor (const entry of list) {\n\t\tconst normalized = normalizeToolName(entry);\n\t\tif (normalized === \"group:plugins\") {\n\t\t\tif (groups.all.length > 0) {\n\t\t\t\texpanded.push(...groups.all);\n\t\t\t} else {\n\t\t\t\texpanded.push(normalized);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tconst tools = groups.byPlugin.get(normalized);\n\t\tif (tools && tools.length > 0) {\n\t\t\texpanded.push(...tools);\n\t\t\tcontinue;\n\t\t}\n\t\texpanded.push(normalized);\n\t}\n\n\treturn Array.from(new Set(expanded));\n}\n\n/**\n * Expand a policy with plugin group resolution.\n *\n * @param policy - The policy to expand\n * @param groups - Plugin tool groups for resolution\n * @returns Policy with plugin groups expanded\n */\nexport function expandPolicyWithPluginGroups(\n\tpolicy: ToolPolicyConfig | undefined,\n\tgroups: PluginToolGroups,\n): ToolPolicyConfig | undefined {\n\tif (!policy) {\n\t\treturn undefined;\n\t}\n\treturn {\n\t\tallow: expandPluginGroups(policy.allow, groups),\n\t\tdeny: expandPluginGroups(policy.deny, groups),\n\t};\n}\n\n/**\n * Strip plugin-only allowlist to prevent accidentally disabling core tools.\n * When an allowlist contains only plugin tools, we remove it to avoid\n * inadvertently blocking core functionality.\n *\n * @param policy - The policy to check\n * @param groups - Plugin tool groups\n * @param coreTools - Set of core tool names\n * @returns Resolution result with diagnostic information\n */\nexport function stripPluginOnlyAllowlist(\n\tpolicy: ToolPolicyConfig | undefined,\n\tgroups: PluginToolGroups,\n\tcoreTools: Set<string>,\n): AllowlistResolution {\n\tif (!policy?.allow || policy.allow.length === 0) {\n\t\treturn { policy, unknownAllowlist: [], strippedAllowlist: false };\n\t}\n\n\tconst normalized = normalizeToolList(policy.allow);\n\tif (normalized.length === 0) {\n\t\treturn { policy, unknownAllowlist: [], strippedAllowlist: false };\n\t}\n\n\tconst pluginIds = new Set(groups.byPlugin.keys());\n\tconst pluginTools = new Set(groups.all);\n\tconst unknownAllowlist: string[] = [];\n\tlet hasCoreEntry = false;\n\n\tfor (const entry of normalized) {\n\t\tif (entry === \"*\") {\n\t\t\thasCoreEntry = true;\n\t\t\tcontinue;\n\t\t}\n\t\tconst isPluginEntry =\n\t\t\tentry === \"group:plugins\" ||\n\t\t\tpluginIds.has(entry) ||\n\t\t\tpluginTools.has(entry);\n\t\tconst expanded = expandToolGroups([entry]);\n\t\tconst isCoreEntry = expanded.some((tool) => coreTools.has(tool));\n\t\tif (isCoreEntry) {\n\t\t\thasCoreEntry = true;\n\t\t}\n\t\tif (!isCoreEntry && !isPluginEntry) {\n\t\t\tunknownAllowlist.push(entry);\n\t\t}\n\t}\n\n\tconst strippedAllowlist = !hasCoreEntry;\n\n\treturn {\n\t\tpolicy: strippedAllowlist ? { ...policy, allow: undefined } : policy,\n\t\tunknownAllowlist: Array.from(new Set(unknownAllowlist)),\n\t\tstrippedAllowlist,\n\t};\n}\n\n/**\n * Merge multiple tool policies into a single effective policy.\n * Later policies take precedence for conflicts.\n *\n * @param policies - Policies to merge in order of precedence\n * @returns Merged policy\n */\nexport function mergeToolPolicies(\n\t...policies: Array<ToolPolicyConfig | undefined>\n): ToolPolicyConfig {\n\tconst result: ToolPolicyConfig = {};\n\n\tfor (const policy of policies) {\n\t\tif (!policy) continue;\n\n\t\tif (policy.allow !== undefined) {\n\t\t\t// If a more specific policy has an allow list, it replaces (not merges)\n\t\t\tresult.allow = [...(policy.allow || [])];\n\t\t}\n\n\t\tif (policy.deny !== undefined) {\n\t\t\t// Deny lists are additive - combine them\n\t\t\tresult.deny = [...(result.deny || []), ...(policy.deny || [])];\n\t\t}\n\t}\n\n\t// Deduplicate\n\tif (result.allow) {\n\t\tresult.allow = Array.from(new Set(result.allow));\n\t}\n\tif (result.deny) {\n\t\tresult.deny = Array.from(new Set(result.deny));\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if a tool is allowed by a policy.\n *\n * @param toolName - The tool name to check\n * @param policy - The policy to evaluate against\n * @returns Whether the tool is allowed\n */\nexport function isToolAllowedByPolicy(\n\ttoolName: string,\n\tpolicy: ToolPolicyConfig | undefined,\n): boolean {\n\tconst normalizedName = normalizeToolName(toolName);\n\n\t// No policy means all tools allowed\n\tif (!policy) {\n\t\treturn true;\n\t}\n\n\t// Check deny list first (deny takes precedence)\n\tif (policy.deny && policy.deny.length > 0) {\n\t\tconst expandedDeny = expandToolGroups(policy.deny);\n\t\tif (expandedDeny.includes(normalizedName)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Check allow list\n\tif (policy.allow && policy.allow.length > 0) {\n\t\tconst expandedAllow = expandToolGroups(policy.allow);\n\t\t// Wildcard allows everything not denied\n\t\tif (expandedAllow.includes(\"*\")) {\n\t\t\treturn true;\n\t\t}\n\t\treturn expandedAllow.includes(normalizedName);\n\t}\n\n\t// No allow list means all tools allowed (if not denied)\n\treturn true;\n}\n",
|
|
460
|
-
"import type { UUID } from \"./primitives\";\n\nexport const TRIGGER_SCHEMA_VERSION = 1 as const;\n\nexport type TriggerType = \"interval\" | \"once\" | \"cron\";\nexport type TriggerWakeMode = \"inject_now\" | \"next_autonomy_cycle\";\nexport type TriggerLastStatus = \"success\" | \"error\" | \"skipped\";\nexport type TriggerKind = \"text\" | \"workflow\";\n\nexport interface TriggerConfig {\n\tversion: typeof TRIGGER_SCHEMA_VERSION;\n\ttriggerId: UUID;\n\tdisplayName: string;\n\tinstructions: string;\n\ttriggerType: TriggerType;\n\tenabled: boolean;\n\twakeMode: TriggerWakeMode;\n\tcreatedBy: string;\n\ttimezone?: string;\n\tintervalMs?: number;\n\tscheduledAtIso?: string;\n\tcronExpression?: string;\n\tmaxRuns?: number;\n\trunCount: number;\n\tnextRunAtMs?: number;\n\tlastRunAtIso?: string;\n\tlastStatus?: TriggerLastStatus;\n\tlastError?: string;\n\tdedupeKey?: string;\n\t// When undefined, treat as \"text\" for back-compat.\n\tkind?: TriggerKind;\n\tworkflowId?: string;\n\tworkflowName?: string;\n}\n\nexport interface TriggerRunRecord {\n\ttriggerRunId: UUID;\n\ttriggerId: UUID;\n\ttaskId: UUID;\n\tstartedAt: number;\n\tfinishedAt: number;\n\tstatus: TriggerLastStatus;\n\terror?: string;\n\tlatencyMs: number;\n\tsource: \"scheduler\" | \"manual\";\n}\n",
|
|
460
|
+
"import type { UUID } from \"./primitives\";\n\nexport const TRIGGER_SCHEMA_VERSION = 1 as const;\n\nexport type TriggerType = \"interval\" | \"once\" | \"cron\" | \"event\";\nexport type TriggerWakeMode = \"inject_now\" | \"next_autonomy_cycle\";\nexport type TriggerLastStatus = \"success\" | \"error\" | \"skipped\";\nexport type TriggerKind = \"text\" | \"workflow\";\n\nexport interface TriggerConfig {\n\tversion: typeof TRIGGER_SCHEMA_VERSION;\n\ttriggerId: UUID;\n\tdisplayName: string;\n\tinstructions: string;\n\ttriggerType: TriggerType;\n\tenabled: boolean;\n\twakeMode: TriggerWakeMode;\n\tcreatedBy: string;\n\ttimezone?: string;\n\tintervalMs?: number;\n\tscheduledAtIso?: string;\n\tcronExpression?: string;\n\teventKind?: string;\n\tmaxRuns?: number;\n\trunCount: number;\n\tnextRunAtMs?: number;\n\tlastRunAtIso?: string;\n\tlastStatus?: TriggerLastStatus;\n\tlastError?: string;\n\tdedupeKey?: string;\n\t// When undefined, treat as \"text\" for back-compat.\n\tkind?: TriggerKind;\n\tworkflowId?: string;\n\tworkflowName?: string;\n}\n\nexport interface TriggerRunRecord {\n\ttriggerRunId: UUID;\n\ttriggerId: UUID;\n\ttaskId: UUID;\n\tstartedAt: number;\n\tfinishedAt: number;\n\tstatus: TriggerLastStatus;\n\terror?: string;\n\tlatencyMs: number;\n\tsource: \"scheduler\" | \"manual\" | \"event\";\n\teventKind?: string;\n}\n",
|
|
461
461
|
"// Core types\n\nexport { logger } from \"../logger\";\n// Utilities that are part of the public API.\nexport { addHeader, composePromptFromState, parseKeyValueXml } from \"../utils\";\nexport * from \"./agent\";\n// Channel configuration types for plugins\nexport * from \"./channel-config\";\nexport * from \"./components\";\nexport * from \"./database\";\nexport * from \"./environment\";\nexport * from \"./events\";\nexport * from \"./hook\";\nexport * from \"./knowledge\";\nexport * from \"./memory\";\nexport * from \"./memory-storage\";\nexport * from \"./messaging\";\nexport * from \"./model\";\n// Onboarding types\nexport * from \"./onboarding\";\nexport * from \"./pairing\";\nexport * from \"./payment\";\nexport * from \"./pipeline-hooks\";\nexport * from \"./plugin\";\nexport * from \"./plugin-store\";\nexport * from \"./primitives\";\nexport * from \"./prompt-batcher\";\nexport * from \"./prompt-optimization-hooks\";\nexport * from \"./prompt-optimization-score-card\";\nexport * from \"./prompt-optimization-trace\";\nexport * from \"./prompts\";\n// Proto-generated types (single source of truth)\n// These types are generated from /schemas/eliza/v1/*.proto\n// Use these for new code and cross-language interoperability\nexport * as proto from \"./proto.js\";\n// Re-export proto utilities for JSON conversion\n// JsonValue is also exported from primitives.ts, but we explicitly export it here for clarity\nexport { fromJson, type JsonObject, type JsonValue, toJson } from \"./proto.js\";\nexport * from \"./runtime\";\nexport * from \"./schema\";\nexport * from \"./schema-builder\";\nexport * from \"./service\";\nexport * from \"./service-interfaces\";\nexport * from \"./settings\";\nexport * from \"./state\";\nexport * from \"./streaming\";\nexport * from \"./task\";\nexport * from \"./tee\";\nexport * from \"./testing\";\nexport * from \"./tools\";\nexport * from \"./trigger\";\n",
|
|
462
462
|
"import { logger } from \"./logger\";\nimport {\n\ttype Entity,\n\ttype IAgentRuntime,\n\ttype Memory,\n\tModelType,\n\ttype Relationship,\n\ttype State,\n\ttype UUID,\n\ttype World,\n} from \"./types\";\nimport * as utils from \"./utils\";\nimport { stableStringify } from \"./utils/deterministic\";\n\ntype EntityDetailsRecord = Pick<Entity, \"id\" | \"names\"> & {\n\tname?: string;\n\tdata: string;\n};\n\nconst ENTITY_DETAILS_CACHE_TTL_MS = 1_000;\nconst entityDetailsCache = new WeakMap<\n\tIAgentRuntime,\n\tMap<string, { expiresAt: number; promise: Promise<EntityDetailsRecord[]> }>\n>();\n\ninterface EntityMatch {\n\tname?: string;\n\treason?: string;\n}\n\ninterface ParsedResolution {\n\tresolvedId?: string;\n\tconfidence?: string;\n\tmatches?: {\n\t\tmatch?: EntityMatch | EntityMatch[];\n\t};\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction normalizeEntityMatch(value: unknown): EntityMatch | null {\n\tif (!isRecord(value)) return null;\n\n\tconst name = typeof value.name === \"string\" ? value.name : undefined;\n\tconst reason = typeof value.reason === \"string\" ? value.reason : undefined;\n\n\tif (!name) return null;\n\treturn { name, reason };\n}\n\nfunction normalizeEntityMatches(value: unknown): EntityMatch[] {\n\tif (Array.isArray(value)) {\n\t\treturn value\n\t\t\t.map((entry) => normalizeEntityMatch(entry))\n\t\t\t.filter((entry): entry is EntityMatch => entry !== null);\n\t}\n\n\tif (isRecord(value) && \"match\" in value) {\n\t\treturn normalizeEntityMatches(value.match);\n\t}\n\n\tconst directMatch = normalizeEntityMatch(value);\n\treturn directMatch ? [directMatch] : [];\n}\n\nfunction parseEntityResolutionResponse(\n\ttext: string,\n): (ParsedResolution & { type?: string; entityId?: string }) | null {\n\tif (!text) return null;\n\n\tconst parsed = utils.parseKeyValueXml<Record<string, unknown>>(text);\n\tconst trimmed = text.trim();\n\n\tif (parsed) {\n\t\tconst type = typeof parsed.type === \"string\" ? parsed.type : undefined;\n\t\tconst entityId =\n\t\t\ttypeof parsed.entityId === \"string\"\n\t\t\t\t? parsed.entityId\n\t\t\t\t: typeof parsed.resolvedId === \"string\"\n\t\t\t\t\t? parsed.resolvedId\n\t\t\t\t\t: undefined;\n\t\tconst matches = normalizeEntityMatches(parsed.matches);\n\n\t\tif (type || entityId || matches.length > 0) {\n\t\t\treturn {\n\t\t\t\ttype,\n\t\t\t\tentityId: entityId && entityId !== \"null\" ? entityId : undefined,\n\t\t\t\tmatches: matches.length > 0 ? { match: matches } : undefined,\n\t\t\t};\n\t\t}\n\t}\n\n\ttry {\n\t\tconst parsedJson = JSON.parse(trimmed) as unknown;\n\t\tif (parsedJson && typeof parsedJson === \"object\") {\n\t\t\tconst obj = parsedJson as Record<string, unknown>;\n\t\t\tconst type = typeof obj.type === \"string\" ? obj.type : undefined;\n\t\t\tconst entityId =\n\t\t\t\ttypeof obj.entityId === \"string\"\n\t\t\t\t\t? obj.entityId\n\t\t\t\t\t: typeof obj.resolvedId === \"string\"\n\t\t\t\t\t\t? obj.resolvedId\n\t\t\t\t\t\t: undefined;\n\t\t\tconst matches = normalizeEntityMatches(obj.matches);\n\n\t\t\tif (type || entityId || matches.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\ttype,\n\t\t\t\t\tentityId: entityId && entityId !== \"null\" ? entityId : undefined,\n\t\t\t\t\tmatches: matches.length > 0 ? { match: matches } : undefined,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// ignore\n\t}\n\n\treturn null;\n}\n\nconst entityResolutionTemplate = `# Task: Resolve Entity Name\nMessage Sender: {{senderName}} (ID: {{senderId}})\nAgent: {{agentName}} (ID: {{agentId}})\n\n# Entities in Room:\n{{#if entitiesInRoom}}\n{{entitiesInRoom}}\n{{/if}}\n\n{{recentMessages}}\n\n# Instructions:\n1. Analyze the context to identify which entity is being referenced\n2. Consider special references like \"me\" (the message sender) or \"you\" (agent the message is directed to)\n3. Look for usernames/handles in standard formats (e.g. @username, user#1234)\n4. Consider context from recent messages for pronouns and references\n5. If multiple matches exist, use context to disambiguate\n6. Consider recent interactions and relationship strength when resolving ambiguity\n\nReturn a TOON document with:\nentityId: exact-id-if-known-otherwise-null\ntype: EXACT_MATCH | USERNAME_MATCH | NAME_MATCH | RELATIONSHIP_MATCH | AMBIGUOUS | UNKNOWN\nmatches[0]:\n name: matched-name\n reason: why this entity matches\n\nIMPORTANT: Your response must ONLY contain the TOON document above. Do not include any text, thinking, or reasoning before or after it.`;\n\nasync function getRecentInteractions(\n\truntime: IAgentRuntime,\n\tsourceEntityId: UUID,\n\tcandidateEntities: Entity[],\n\troomId: UUID,\n\trelationships: Relationship[],\n): Promise<{ entity: Entity; interactions: Memory[]; count: number }[]> {\n\tconst results: Array<{\n\t\tentity: Entity;\n\t\tinteractions: Memory[];\n\t\tcount: number;\n\t}> = [];\n\n\tconst recentMessages = await runtime.getMemories({\n\t\ttableName: \"messages\",\n\t\troomId,\n\t\tlimit: 20,\n\t});\n\tconst messageEntityById = new Map<UUID, UUID>();\n\tfor (const recentMessage of recentMessages) {\n\t\tif (recentMessage.id && recentMessage.entityId) {\n\t\t\tmessageEntityById.set(recentMessage.id, recentMessage.entityId);\n\t\t}\n\t}\n\n\tfor (const entity of candidateEntities) {\n\t\tconst interactions: Memory[] = [];\n\t\tlet interactionScore = 0;\n\n\t\tconst directReplies = recentMessages.filter((msg) => {\n\t\t\tif (!msg.entityId || !msg.content.inReplyTo) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst repliedToEntityId = messageEntityById.get(msg.content.inReplyTo);\n\t\t\treturn (\n\t\t\t\t(msg.entityId === sourceEntityId && repliedToEntityId === entity.id) ||\n\t\t\t\t(msg.entityId === entity.id && repliedToEntityId === sourceEntityId)\n\t\t\t);\n\t\t});\n\n\t\tinteractions.push(...directReplies);\n\n\t\tconst relationship = relationships.find(\n\t\t\t(rel) =>\n\t\t\t\t(rel.sourceEntityId === sourceEntityId &&\n\t\t\t\t\trel.targetEntityId === entity.id) ||\n\t\t\t\t(rel.targetEntityId === sourceEntityId &&\n\t\t\t\t\trel.sourceEntityId === entity.id),\n\t\t);\n\n\t\tconst relationshipMetadata = relationship?.metadata;\n\t\tif (relationshipMetadata?.interactions) {\n\t\t\tinteractionScore = relationshipMetadata.interactions as number;\n\t\t}\n\n\t\tinteractionScore += directReplies.length;\n\n\t\tconst uniqueInteractions = [...new Set(interactions)];\n\t\tresults.push({\n\t\t\tentity,\n\t\t\tinteractions: uniqueInteractions.slice(-5),\n\t\t\tcount: Math.round(interactionScore),\n\t\t});\n\t}\n\n\treturn results.sort((a, b) => b.count - a.count);\n}\n\nexport async function findEntityByName(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n\tstate: State,\n): Promise<Entity | null> {\n\tconst room = state.data.room ?? (await runtime.getRoom(message.roomId));\n\tif (!room) {\n\t\tlogger.warn(\n\t\t\t{ src: \"core:entities\", roomId: message.roomId },\n\t\t\t\"Room not found for entity search\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst world: World | null = room.worldId\n\t\t? await runtime.getWorld(room.worldId)\n\t\t: null;\n\n\tconst entitiesInRoom = await runtime.getEntitiesForRoom(room.id, true);\n\n\tconst filteredEntities = await Promise.all(\n\t\tentitiesInRoom.map(async (entity) => {\n\t\t\tif (!entity.components) return entity;\n\n\t\t\tconst worldMetadata = world?.metadata;\n\t\t\tconst worldRoles = worldMetadata?.roles || {};\n\n\t\t\tentity.components = entity.components.filter((component) => {\n\t\t\t\tif (component.sourceEntityId === message.entityId) return true;\n\n\t\t\t\tif (world && component.sourceEntityId) {\n\t\t\t\t\tconst sourceRole = worldRoles[component.sourceEntityId];\n\t\t\t\t\tif (sourceRole === \"OWNER\" || sourceRole === \"ADMIN\") return true;\n\t\t\t\t}\n\n\t\t\t\tif (component.sourceEntityId === runtime.agentId) return true;\n\n\t\t\t\treturn false;\n\t\t\t});\n\n\t\t\treturn entity;\n\t\t}),\n\t);\n\n\tconst relationships = await runtime.getRelationships({\n\t\tentityIds: [message.entityId],\n\t});\n\n\tconst relationshipEntities = await Promise.all(\n\t\trelationships.map(async (rel) => {\n\t\t\tconst entityId =\n\t\t\t\trel.sourceEntityId === message.entityId\n\t\t\t\t\t? rel.targetEntityId\n\t\t\t\t\t: rel.sourceEntityId;\n\t\t\treturn runtime.getEntityById(entityId);\n\t\t}),\n\t);\n\n\tconst allEntities = [\n\t\t...filteredEntities,\n\t\t...relationshipEntities.filter((e): e is Entity => e !== null),\n\t];\n\n\tconst interactionData = await getRecentInteractions(\n\t\truntime,\n\t\tmessage.entityId,\n\t\tallEntities,\n\t\troom.id as UUID,\n\t\trelationships,\n\t);\n\n\tconst prompt = utils.composePrompt({\n\t\tstate: {\n\t\t\troomName: (room.name || room.id) as string,\n\t\t\tworldName: (world?.name || \"Unknown\") as string,\n\t\t\tentitiesInRoom: JSON.stringify(filteredEntities, null, 2),\n\t\t\tentityId: message.entityId,\n\t\t\tsenderId: message.entityId,\n\t\t},\n\t\ttemplate: entityResolutionTemplate,\n\t});\n\n\tconst result = await runtime.useModel(ModelType.TEXT_SMALL, {\n\t\tprompt,\n\t\tstopSequences: [],\n\t});\n\n\tconst resolution = parseEntityResolutionResponse(result);\n\tif (!resolution) {\n\t\t// If the model output is malformed, fall back to a conservative heuristic:\n\t\t// when there's only one candidate entity in context, return it.\n\t\tif (filteredEntities.length === 1) {\n\t\t\treturn filteredEntities[0] ?? null;\n\t\t}\n\t\tlogger.warn(\n\t\t\t{ src: \"core:entities\" },\n\t\t\t\"Failed to parse entity resolution result\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tif (resolution.type === \"EXACT_MATCH\" && resolution.entityId) {\n\t\tconst entity = await runtime.getEntityById(resolution.entityId as UUID);\n\t\tif (entity) {\n\t\t\tif (entity.components) {\n\t\t\t\tconst worldMetadata = world?.metadata;\n\t\t\t\tconst worldRoles = worldMetadata?.roles || {};\n\t\t\t\tentity.components = entity.components.filter((component) => {\n\t\t\t\t\tif (component.sourceEntityId === message.entityId) return true;\n\t\t\t\t\tif (world && component.sourceEntityId) {\n\t\t\t\t\t\tconst sourceRole = worldRoles[component.sourceEntityId];\n\t\t\t\t\t\tif (sourceRole === \"OWNER\" || sourceRole === \"ADMIN\") return true;\n\t\t\t\t\t}\n\t\t\t\t\tif (component.sourceEntityId === runtime.agentId) return true;\n\t\t\t\t\treturn false;\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn entity;\n\t\t}\n\t}\n\n\tlet matchesArray: EntityMatch[] = [];\n\tconst parsedResolution = resolution as ParsedResolution;\n\tconst parsedResolutionMatches = parsedResolution.matches;\n\tif (parsedResolutionMatches?.match) {\n\t\tconst matchValue = parsedResolutionMatches.match;\n\t\tmatchesArray = Array.isArray(matchValue) ? matchValue : [matchValue];\n\t}\n\n\tconst normalize = (s: string): string => s.trim().toLowerCase();\n\tconst stripAt = (s: string): string => normalize(s).replace(/^@+/, \"\");\n\tconst indexedEntities = allEntities.map((entity) => {\n\t\tconst normalizedNames = new Set<string>();\n\t\tconst strippedNames = new Set<string>();\n\t\tfor (const name of entity.names) {\n\t\t\tnormalizedNames.add(normalize(name));\n\t\t\tstrippedNames.add(stripAt(name));\n\t\t}\n\n\t\tconst normalizedUsernames = new Set<string>();\n\t\tconst strippedUsernames = new Set<string>();\n\t\tconst normalizedHandles = new Set<string>();\n\t\tconst strippedHandles = new Set<string>();\n\t\tconst fallbackTokens: string[] = [];\n\t\tfor (const component of entity.components ?? []) {\n\t\t\tconst username =\n\t\t\t\ttypeof component.data?.username === \"string\"\n\t\t\t\t\t? component.data.username\n\t\t\t\t\t: undefined;\n\t\t\tif (username) {\n\t\t\t\tnormalizedUsernames.add(normalize(username));\n\t\t\t\tstrippedUsernames.add(stripAt(username));\n\t\t\t\tfallbackTokens.push(normalize(username));\n\t\t\t}\n\n\t\t\tconst handle =\n\t\t\t\ttypeof component.data?.handle === \"string\"\n\t\t\t\t\t? component.data.handle\n\t\t\t\t\t: undefined;\n\t\t\tif (handle) {\n\t\t\t\tconst normalizedHandle = normalize(handle);\n\t\t\t\tnormalizedHandles.add(normalizedHandle);\n\t\t\t\tstrippedHandles.add(stripAt(handle));\n\t\t\t\tfallbackTokens.push(normalizedHandle);\n\t\t\t\tconst handleNoAt = handle.replace(/^@+/, \"\");\n\t\t\t\tif (handleNoAt) {\n\t\t\t\t\tfallbackTokens.push(normalize(handleNoAt));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tentity,\n\t\t\tnormalizedNames,\n\t\t\tstrippedNames,\n\t\t\tnormalizedUsernames,\n\t\t\tstrippedUsernames,\n\t\t\tnormalizedHandles,\n\t\t\tstrippedHandles,\n\t\t\tfallbackTokens,\n\t\t};\n\t});\n\n\tconst firstMatch = matchesArray[0];\n\tif (matchesArray.length > 0 && firstMatch && firstMatch.name) {\n\t\tconst matchName = normalize(firstMatch.name);\n\t\tconst matchKey = stripAt(firstMatch.name);\n\n\t\tconst matchingEntity = indexedEntities.find((entry) => {\n\t\t\tif (\n\t\t\t\tentry.strippedNames.has(matchKey) ||\n\t\t\t\tentry.normalizedNames.has(matchName) ||\n\t\t\t\tentry.strippedUsernames.has(matchKey) ||\n\t\t\t\tentry.normalizedUsernames.has(matchName) ||\n\t\t\t\tentry.strippedHandles.has(matchKey) ||\n\t\t\t\tentry.normalizedHandles.has(matchName)\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t})?.entity;\n\n\t\tif (matchingEntity) {\n\t\t\tif (resolution.type === \"RELATIONSHIP_MATCH\") {\n\t\t\t\tconst interactionInfo = interactionData.find(\n\t\t\t\t\t(d) => d.entity.id === matchingEntity.id,\n\t\t\t\t);\n\t\t\t\tif (interactionInfo && interactionInfo.count > 0) {\n\t\t\t\t\treturn matchingEntity;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn matchingEntity;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: if parsing failed to produce a usable match list, try to detect\n\t// usernames/handles mentioned in the raw model output.\n\tconst resultLower = result.toLowerCase();\n\tconst fallbackEntity = indexedEntities.find((entry) =>\n\t\tentry.fallbackTokens.some((token) => resultLower.includes(token)),\n\t)?.entity;\n\tif (fallbackEntity) {\n\t\treturn fallbackEntity;\n\t}\n\n\t// Heuristic fallback: if the model indicates a name/username match but we\n\t// couldn't map it, and there's only a single candidate entity in context,\n\t// return it rather than failing closed.\n\tif (\n\t\t(resolution.type === \"USERNAME_MATCH\" ||\n\t\t\tresolution.type === \"NAME_MATCH\") &&\n\t\tfilteredEntities.length === 1\n\t) {\n\t\treturn filteredEntities[0] ?? null;\n\t}\n\n\t// Final fallback: if there's only one candidate entity in scope, return it.\n\t// This prevents needless nulls in small rooms when the model response is noisy.\n\tif (allEntities.length === 1) {\n\t\treturn allEntities[0] ?? null;\n\t}\n\n\treturn null;\n}\n\nexport const createUniqueUuid = (\n\truntime: IAgentRuntime,\n\tbaseUserId: UUID | string,\n): UUID => {\n\tif (baseUserId === runtime.agentId) {\n\t\treturn runtime.agentId;\n\t}\n\n\tconst combinedString = `${baseUserId}:${runtime.agentId}`;\n\treturn utils.stringToUuid(combinedString);\n};\n\nexport async function getEntityDetails({\n\truntime,\n\troomId,\n}: {\n\truntime: IAgentRuntime;\n\troomId: UUID;\n}) {\n\tconst runtimeCache = entityDetailsCache.get(runtime) ?? new Map();\n\tentityDetailsCache.set(runtime, runtimeCache);\n\n\tconst cacheKey = String(roomId);\n\tconst cachedEntry = runtimeCache.get(cacheKey);\n\tif (cachedEntry && cachedEntry.expiresAt > Date.now()) {\n\t\treturn cachedEntry.promise;\n\t}\n\n\tconst pendingPromise = (async () => {\n\t\tconst [room, roomEntities] = await Promise.all([\n\t\t\truntime.getRoom(roomId),\n\t\t\truntime.getEntitiesForRoom(roomId, true),\n\t\t]);\n\n\t\tconst uniqueEntities = new Map<string, EntityDetailsRecord>();\n\n\t\tfor (const entity of roomEntities) {\n\t\t\tconst entityId = entity.id;\n\t\t\tif (!entityId || uniqueEntities.has(entityId)) continue;\n\n\t\t\tconst allData = {};\n\t\t\tfor (const component of entity.components || []) {\n\t\t\t\tObject.assign(allData, component.data);\n\t\t\t}\n\n\t\t\tconst mergedData: Record<string, unknown> = {};\n\t\t\tfor (const [key, value] of Object.entries(allData)) {\n\t\t\t\tif (!mergedData[key]) {\n\t\t\t\t\tmergedData[key] = value;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(mergedData[key]) && Array.isArray(value)) {\n\t\t\t\t\tmergedData[key] = [...new Set([...mergedData[key], ...value])];\n\t\t\t\t} else if (\n\t\t\t\t\ttypeof mergedData[key] === \"object\" &&\n\t\t\t\t\ttypeof value === \"object\"\n\t\t\t\t) {\n\t\t\t\t\tmergedData[key] = { ...mergedData[key], ...value };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst getEntityNameFromMetadata = (\n\t\t\t\tsource: string,\n\t\t\t): string | undefined => {\n\t\t\t\tconst sourceMetadata = entity.metadata?.[source];\n\t\t\t\tif (\n\t\t\t\t\tsourceMetadata &&\n\t\t\t\t\ttypeof sourceMetadata === \"object\" &&\n\t\t\t\t\tsourceMetadata !== null\n\t\t\t\t) {\n\t\t\t\t\tconst metadataObj = sourceMetadata as Record<string, unknown>;\n\t\t\t\t\tif (\"name\" in metadataObj && typeof metadataObj.name === \"string\") {\n\t\t\t\t\t\treturn metadataObj.name;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t};\n\n\t\t\tuniqueEntities.set(entityId, {\n\t\t\t\tid: entityId,\n\t\t\t\tname: room?.source\n\t\t\t\t\t? getEntityNameFromMetadata(String(room.source)) || entity.names[0]\n\t\t\t\t\t: entity.names[0],\n\t\t\t\tnames: entity.names,\n\t\t\t\tdata: stableStringify({ ...mergedData, ...entity.metadata }),\n\t\t\t});\n\t\t}\n\n\t\treturn Array.from(uniqueEntities.values()).sort((left, right) => {\n\t\t\tconst leftName = left.name ?? left.names[0] ?? \"\";\n\t\t\tconst rightName = right.name ?? right.names[0] ?? \"\";\n\t\t\treturn (\n\t\t\t\tleftName.localeCompare(rightName) ||\n\t\t\t\tString(left.id ?? \"\").localeCompare(String(right.id ?? \"\"))\n\t\t\t);\n\t\t});\n\t})();\n\n\truntimeCache.set(cacheKey, {\n\t\texpiresAt: Date.now() + ENTITY_DETAILS_CACHE_TTL_MS,\n\t\tpromise: pendingPromise,\n\t});\n\n\ttry {\n\t\treturn await pendingPromise;\n\t} catch (error) {\n\t\truntimeCache.delete(cacheKey);\n\t\tthrow error;\n\t}\n}\n\nexport function formatEntities({ entities }: { entities: Entity[] }) {\n\tconst sortedEntities = [...entities].sort((left, right) => {\n\t\tconst leftName = left.names[0] ?? \"\";\n\t\tconst rightName = right.names[0] ?? \"\";\n\t\treturn (\n\t\t\tleftName.localeCompare(rightName) ||\n\t\t\tString(left.id ?? \"\").localeCompare(String(right.id ?? \"\"))\n\t\t);\n\t});\n\n\tconst entityStrings = sortedEntities.map((entity: Entity) => {\n\t\tconst header = `\"${entity.names.join('\" aka \"')}\"\\nID: ${entity.id}${\n\t\t\tentity.metadata && Object.keys(entity.metadata).length > 0\n\t\t\t\t? `\\nData: ${stableStringify(entity.metadata)}\\n`\n\t\t\t\t: \"\\n\"\n\t\t}`;\n\t\treturn header;\n\t});\n\treturn entityStrings.join(\"\\n\");\n}\n",
|
|
463
463
|
"import { createUniqueUuid } from \"./entities\";\nimport { logger } from \"./logger\";\nimport type { IAgentRuntime, Memory, Role, UUID, World } from \"./types\";\n\nconst DEFAULT_SERVER_ROLE: Role = \"NONE\";\n\nexport type RoleName = \"OWNER\" | \"ADMIN\" | \"USER\" | \"GUEST\";\n\nexport type RoleGrantSource = \"owner\" | \"manual\" | \"connector_admin\";\n\nexport const ROLE_RANK: Record<RoleName, number> = {\n\tGUEST: 0,\n\tUSER: 1,\n\tADMIN: 2,\n\tOWNER: 3,\n};\n\nexport type RolesWorldMetadata = {\n\townership?: { ownerId?: string };\n\troles?: Record<string, RoleName>;\n\troleSources?: Record<string, RoleGrantSource>;\n};\n\nexport type ConnectorAdminWhitelist = Record<string, string[]>;\n\nexport type RolesConfig = {\n\tconnectorAdmins?: ConnectorAdminWhitelist;\n};\n\nexport type RoleCheckResult = {\n\tentityId: UUID;\n\trole: RoleName;\n\tisOwner: boolean;\n\tisAdmin: boolean;\n\tcanManageRoles: boolean;\n};\n\nexport interface ServerOwnershipState {\n\tservers: {\n\t\t[serverId: string]: World;\n\t};\n}\n\nconst CONNECTOR_ADMINS_SETTING_KEY = \"ELIZA_ROLES_CONNECTOR_ADMINS_JSON\";\nconst CANONICAL_OWNER_SETTING_KEY = \"ELIZA_ADMIN_ENTITY_ID\";\nconst OWNER_CONTACTS_SETTING_KEY = \"ELIZA_OWNER_CONTACTS_JSON\";\nconst CONNECTOR_ID_FIELDS = [\"userId\", \"id\", \"username\", \"userName\"] as const;\nconst CONNECTOR_STABLE_ID_FIELDS = [\"userId\", \"id\"] as const;\ntype ConnectorIdField = (typeof CONNECTOR_ID_FIELDS)[number];\ntype ConnectorAdminMatch = {\n\tconnector: string;\n\tmatchedValue: string;\n\tmatchedField: ConnectorIdField;\n};\n\ntype ResolveEntityRoleOptions = {\n\tliveEntityMetadata?: Record<string, unknown> | null;\n\tliveEntityId?: string;\n};\n\ntype OwnerContactEntry = {\n\tentityId?: string;\n};\n\nfunction asStringArray(value: unknown): string[] {\n\tif (!Array.isArray(value)) return [];\n\treturn value\n\t\t.filter((entry): entry is string => typeof entry === \"string\")\n\t\t.map((entry) => entry.trim())\n\t\t.filter(Boolean);\n}\n\nfunction normalizeConnectorAdminWhitelist(\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): ConnectorAdminWhitelist {\n\tif (!whitelist || typeof whitelist !== \"object\") return {};\n\n\treturn Object.fromEntries(\n\t\tObject.entries(whitelist)\n\t\t\t.map(([connector, values]) => [connector, asStringArray(values)])\n\t\t\t.filter(([, values]) => values.length > 0),\n\t);\n}\n\nfunction normalizeRoleGrantSource(\n\traw: string | undefined | null,\n): RoleGrantSource | null {\n\tif (raw === \"owner\" || raw === \"manual\" || raw === \"connector_admin\") {\n\t\treturn raw;\n\t}\n\treturn null;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn undefined;\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction formatError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction getRuntimeSettingString(\n\truntime: IAgentRuntime,\n\tkey: string,\n): string | undefined {\n\tif (typeof runtime.getSetting !== \"function\") {\n\t\treturn undefined;\n\t}\n\n\tconst value = runtime.getSetting(key);\n\tif (typeof value !== \"string\") {\n\t\treturn undefined;\n\t}\n\n\tconst trimmed = value.trim();\n\treturn trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction parseOwnerContactEntityIds(raw: string | undefined): string[] {\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Record<string, OwnerContactEntry>;\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\t\treturn [];\n\t\t}\n\n\t\treturn Object.values(parsed)\n\t\t\t.map((entry) =>\n\t\t\t\tentry && typeof entry.entityId === \"string\"\n\t\t\t\t\t? entry.entityId.trim()\n\t\t\t\t\t: \"\",\n\t\t\t)\n\t\t\t.filter((entityId) => entityId.length > 0);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to parse owner contacts from runtime settings: ${formatError(error)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\nfunction getMemoryMetadata(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\treturn asRecord((message as Memory & { metadata?: unknown }).metadata);\n}\n\nfunction getMessageSource(message: Memory): string | undefined {\n\treturn typeof message.content?.source === \"string\"\n\t\t? message.content.source\n\t\t: undefined;\n}\n\nfunction getConnectorMetadataFromMemory(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\tconst memoryMetadata = getMemoryMetadata(message);\n\tconst source = getMessageSource(message);\n\tif (!source) {\n\t\treturn undefined;\n\t}\n\n\tconst sourceMetadata = asRecord(memoryMetadata?.[source]);\n\tif (sourceMetadata) {\n\t\treturn { [source]: sourceMetadata };\n\t}\n\n\tif (source === \"discord\") {\n\t\tconst fromId = memoryMetadata?.fromId;\n\t\tif (typeof fromId !== \"string\" || fromId.trim().length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst entityName =\n\t\t\ttypeof memoryMetadata?.entityName === \"string\"\n\t\t\t\t? memoryMetadata.entityName\n\t\t\t\t: undefined;\n\n\t\treturn {\n\t\t\tdiscord: {\n\t\t\t\tuserId: fromId,\n\t\t\t\tid: fromId,\n\t\t\t\t...(entityName ? { name: entityName, username: entityName } : {}),\n\t\t\t},\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\nasync function getEntityMetadata(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<Record<string, unknown> | undefined> {\n\tif (typeof runtime.getEntityById !== \"function\") {\n\t\treturn undefined;\n\t}\n\n\ttry {\n\t\tconst entity = await runtime.getEntityById(entityId as UUID);\n\t\treturn asRecord(entity?.metadata);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to look up entity ${entityId}: ${formatError(error)}`,\n\t\t);\n\t\treturn undefined;\n\t}\n}\n\nexport async function getUserServerRole(\n\truntime: IAgentRuntime,\n\tentityId: string,\n\tserverId: string,\n): Promise<Role> {\n\tconst worldId = createUniqueUuid(runtime, serverId);\n\tconst world = await runtime.getWorld(worldId);\n\n\tconst worldMetadata = world?.metadata;\n\tconst roles = worldMetadata?.roles;\n\tif (!roles) {\n\t\treturn DEFAULT_SERVER_ROLE;\n\t}\n\n\tconst role = roles[entityId as UUID];\n\tif (role) {\n\t\treturn role;\n\t}\n\n\treturn DEFAULT_SERVER_ROLE;\n}\n\nexport async function findWorldsForOwner(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<World[] | null> {\n\tif (!entityId) {\n\t\tlogger.error(\n\t\t\t{ src: \"core:roles\", agentId: runtime.agentId },\n\t\t\t\"User ID is required to find server\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst worlds = await runtime.getAllWorlds();\n\n\tif (!worlds || worlds.length === 0) {\n\t\tlogger.debug(\n\t\t\t{ src: \"core:roles\", agentId: runtime.agentId },\n\t\t\t\"No worlds found for agent\",\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst ownerWorlds: World[] = [];\n\tfor (const world of worlds) {\n\t\tconst worldMetadata = world.metadata;\n\t\tconst worldMetadataOwnership = worldMetadata?.ownership;\n\t\tif (worldMetadataOwnership && worldMetadataOwnership.ownerId === entityId) {\n\t\t\townerWorlds.push(world);\n\t\t}\n\t}\n\n\treturn ownerWorlds.length ? ownerWorlds : null;\n}\n\nexport function getConfiguredOwnerEntityIds(runtime: IAgentRuntime): string[] {\n\tconst configuredAdminEntityId = getRuntimeSettingString(\n\t\truntime,\n\t\tCANONICAL_OWNER_SETTING_KEY,\n\t);\n\tconst ownerContactsRaw = getRuntimeSettingString(\n\t\truntime,\n\t\tOWNER_CONTACTS_SETTING_KEY,\n\t);\n\tconst ownerContactEntityIds = parseOwnerContactEntityIds(ownerContactsRaw);\n\tconst deduped = new Set<string>();\n\n\tif (configuredAdminEntityId) {\n\t\tdeduped.add(configuredAdminEntityId);\n\t}\n\n\tfor (const entityId of ownerContactEntityIds) {\n\t\tdeduped.add(entityId);\n\t}\n\n\treturn [...deduped];\n}\n\nexport function hasConfiguredCanonicalOwner(runtime: IAgentRuntime): boolean {\n\treturn getConfiguredOwnerEntityIds(runtime).length > 0;\n}\n\nexport function resolveCanonicalOwnerId(\n\truntime: IAgentRuntime,\n\tmetadata?: RolesWorldMetadata,\n): string | null {\n\tconst configuredOwnerIds = getConfiguredOwnerEntityIds(runtime);\n\tif (configuredOwnerIds.length > 0) {\n\t\treturn configuredOwnerIds[0] ?? null;\n\t}\n\n\tconst worldOwnerId = metadata?.ownership?.ownerId;\n\treturn typeof worldOwnerId === \"string\" && worldOwnerId.length > 0\n\t\t? worldOwnerId\n\t\t: null;\n}\n\nfunction resolveOwnershipCandidateIds(\n\truntime: IAgentRuntime,\n\tmetadata?: RolesWorldMetadata,\n): string[] {\n\tconst configuredOwnerIds = getConfiguredOwnerEntityIds(runtime);\n\tif (configuredOwnerIds.length > 0) {\n\t\treturn configuredOwnerIds;\n\t}\n\n\tconst ownerId = resolveCanonicalOwnerId(runtime, metadata);\n\treturn ownerId ? [ownerId] : [];\n}\n\nfunction connectorIdentityMatches(\n\tleft: Record<string, unknown> | null | undefined,\n\tright: Record<string, unknown> | null | undefined,\n): boolean {\n\tif (!left || !right) return false;\n\n\tfor (const [connector, leftRaw] of Object.entries(left)) {\n\t\tconst leftConnector = asRecord(leftRaw);\n\t\tconst rightConnector = asRecord(right[connector]);\n\t\tif (!leftConnector || !rightConnector) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const field of CONNECTOR_STABLE_ID_FIELDS) {\n\t\t\tconst leftValue = leftConnector[field];\n\t\t\tconst rightValue = rightConnector[field];\n\t\t\tif (\n\t\t\t\ttypeof leftValue === \"string\" &&\n\t\t\t\tleftValue.length > 0 &&\n\t\t\t\tleftValue === rightValue\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false;\n}\n\nasync function hasConfirmedIdentityLink(\n\truntime: IAgentRuntime,\n\tentityId: string,\n\townerId: string,\n): Promise<boolean> {\n\tconst linkedIds = await getConfirmedLinkedEntityIds(runtime, entityId);\n\treturn linkedIds.includes(ownerId);\n}\n\nasync function getConfirmedLinkedEntityIds(\n\truntime: IAgentRuntime,\n\tentityId: string,\n): Promise<string[]> {\n\tif (typeof runtime.getRelationships !== \"function\") {\n\t\treturn [];\n\t}\n\n\ttry {\n\t\tconst relationships = await runtime.getRelationships({\n\t\t\tentityIds: [entityId as UUID],\n\t\t\ttags: [\"identity_link\"],\n\t\t});\n\n\t\tconst linkedIds = new Set<string>();\n\t\tfor (const relationship of relationships) {\n\t\t\tconst metadata = asRecord(relationship.metadata);\n\t\t\tif (metadata?.status !== \"confirmed\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\trelationship.sourceEntityId === entityId &&\n\t\t\t\ttypeof relationship.targetEntityId === \"string\"\n\t\t\t) {\n\t\t\t\tlinkedIds.add(relationship.targetEntityId);\n\t\t\t}\n\t\t\tif (\n\t\t\t\trelationship.targetEntityId === entityId &&\n\t\t\t\ttypeof relationship.sourceEntityId === \"string\"\n\t\t\t) {\n\t\t\t\tlinkedIds.add(relationship.sourceEntityId);\n\t\t\t}\n\t\t}\n\n\t\treturn [...linkedIds];\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to load identity links for ${entityId}: ${formatError(error)}`,\n\t\t);\n\t\treturn [];\n\t}\n}\n\nasync function resolveOwnershipRole(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleName | null> {\n\tconst ownerIds = resolveOwnershipCandidateIds(runtime, metadata);\n\tif (ownerIds.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst senderMetadata =\n\t\toptions?.liveEntityMetadata ?? (await getEntityMetadata(runtime, entityId));\n\n\tfor (const ownerId of ownerIds) {\n\t\tif (ownerId === entityId) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\n\t\tif (await hasConfirmedIdentityLink(runtime, entityId, ownerId)) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\n\t\tconst ownerMetadata = await getEntityMetadata(runtime, ownerId);\n\t\tif (!ownerMetadata) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (connectorIdentityMatches(senderMetadata, ownerMetadata)) {\n\t\t\treturn \"OWNER\";\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction resolveWorldIdFromMessageMetadata(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): UUID | null {\n\tconst source = getMessageSource(message);\n\tconst metadata = getMemoryMetadata(message);\n\tif (source === \"discord\") {\n\t\tconst serverId =\n\t\t\ttypeof metadata?.discordServerId === \"string\"\n\t\t\t\t? metadata.discordServerId\n\t\t\t\t: typeof metadata?.discordChannelId === \"string\"\n\t\t\t\t\t? metadata.discordChannelId\n\t\t\t\t\t: null;\n\n\t\tif (!serverId) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn createUniqueUuid(runtime, serverId) as UUID;\n\t}\n\n\treturn null;\n}\n\nexport function setConnectorAdminWhitelist(\n\truntime: IAgentRuntime,\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): void {\n\tif (typeof runtime.setSetting !== \"function\") {\n\t\treturn;\n\t}\n\n\tconst normalized = normalizeConnectorAdminWhitelist(whitelist);\n\tif (Object.keys(normalized).length === 0) {\n\t\truntime.setSetting(CONNECTOR_ADMINS_SETTING_KEY, null);\n\t\treturn;\n\t}\n\n\truntime.setSetting(CONNECTOR_ADMINS_SETTING_KEY, JSON.stringify(normalized));\n}\n\nexport function getConnectorAdminWhitelist(\n\truntime: IAgentRuntime,\n): ConnectorAdminWhitelist {\n\tconst raw = getRuntimeSettingString(runtime, CONNECTOR_ADMINS_SETTING_KEY);\n\tif (!raw) {\n\t\treturn {};\n\t}\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Record<string, unknown>;\n\t\treturn normalizeConnectorAdminWhitelist(parsed);\n\t} catch (error) {\n\t\tlogger.warn(\n\t\t\t`[roles] Failed to parse ${CONNECTOR_ADMINS_SETTING_KEY}: ${formatError(error)}`,\n\t\t);\n\t\treturn {};\n\t}\n}\n\nexport function matchEntityToConnectorAdminWhitelist(\n\tentityMetadata: Record<string, unknown> | null | undefined,\n\twhitelist: ConnectorAdminWhitelist | Record<string, unknown> | undefined,\n): ConnectorAdminMatch | null {\n\tif (!entityMetadata || typeof entityMetadata !== \"object\") return null;\n\n\tconst normalizedWhitelist = normalizeConnectorAdminWhitelist(whitelist);\n\tfor (const [connector, platformIds] of Object.entries(normalizedWhitelist)) {\n\t\tconst connectorMeta = asRecord(entityMetadata[connector]);\n\t\tif (!connectorMeta) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const field of CONNECTOR_ID_FIELDS) {\n\t\t\tconst value = connectorMeta[field];\n\t\t\tif (typeof value === \"string\" && platformIds.includes(value)) {\n\t\t\t\treturn { connector, matchedValue: value, matchedField: field };\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\nexport function normalizeRole(raw: string | undefined | null): RoleName {\n\tconst upper = (raw ?? \"\").toUpperCase();\n\tif (upper === \"OWNER\" || upper === \"ADMIN\" || upper === \"USER\") return upper;\n\treturn \"GUEST\";\n}\n\nexport function getEntityRole(\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n): RoleName {\n\tif (!metadata?.roles) return \"GUEST\";\n\treturn normalizeRole(metadata.roles[entityId]);\n}\n\nfunction getStoredRoleSource(\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n): RoleGrantSource | null {\n\treturn normalizeRoleGrantSource(metadata?.roleSources?.[entityId]);\n}\n\nasync function resolveStoredRoleSource(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleGrantSource | null> {\n\tconst storedSource = getStoredRoleSource(metadata, entityId);\n\tif (storedSource) {\n\t\treturn storedSource;\n\t}\n\n\tconst storedRole = getEntityRole(metadata, entityId);\n\tif (storedRole === \"GUEST\") {\n\t\treturn null;\n\t}\n\tif (storedRole === \"OWNER\") {\n\t\treturn \"owner\";\n\t}\n\n\tconst entityMetadata =\n\t\toptions?.liveEntityId === entityId\n\t\t\t? (options.liveEntityMetadata ?? undefined)\n\t\t\t: undefined;\n\tconst matchedWhitelist = matchEntityToConnectorAdminWhitelist(\n\t\tentityMetadata ?? (await getEntityMetadata(runtime, entityId)),\n\t\tgetConnectorAdminWhitelist(runtime),\n\t);\n\n\tif (storedRole === \"ADMIN\" && matchedWhitelist) {\n\t\treturn \"connector_admin\";\n\t}\n\n\treturn \"manual\";\n}\n\nasync function resolveExplicitGrantedRole(\n\truntime: IAgentRuntime,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<{\n\trole: RoleName;\n\tsource: \"manual\" | \"linked_manual\";\n} | null> {\n\tconst directRole = getEntityRole(metadata, entityId);\n\tconst directSource = await resolveStoredRoleSource(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tif (directRole !== \"GUEST\" && directSource === \"manual\") {\n\t\treturn { role: directRole, source: \"manual\" };\n\t}\n\n\tconst linkedIds = await getConfirmedLinkedEntityIds(runtime, entityId);\n\tlet bestRole: RoleName | null = null;\n\n\tfor (const linkedEntityId of linkedIds) {\n\t\tconst linkedRole = getEntityRole(metadata, linkedEntityId);\n\t\tif (linkedRole === \"GUEST\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst linkedSource = await resolveStoredRoleSource(\n\t\t\truntime,\n\t\t\tmetadata,\n\t\t\tlinkedEntityId,\n\t\t);\n\t\tif (linkedSource !== \"manual\") {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!bestRole || ROLE_RANK[linkedRole] > ROLE_RANK[bestRole]) {\n\t\t\tbestRole = linkedRole;\n\t\t}\n\t}\n\n\treturn bestRole ? { role: bestRole, source: \"linked_manual\" } : null;\n}\n\nexport function getLiveEntityMetadataFromMessage(\n\tmessage: Memory,\n): Record<string, unknown> | undefined {\n\t// Only trust connector identity stamped into the Memory itself.\n\t// content.metadata can come from untrusted chat clients, so it must not\n\t// participate in role resolution.\n\treturn getConnectorMetadataFromMemory(message);\n}\n\nexport async function resolveEntityRole(\n\truntime: IAgentRuntime,\n\t_world: Awaited<ReturnType<IAgentRuntime[\"getWorld\"]>>,\n\tmetadata: RolesWorldMetadata | undefined,\n\tentityId: string,\n\toptions?: ResolveEntityRoleOptions,\n): Promise<RoleName> {\n\tconst explicitRole = getEntityRole(metadata, entityId);\n\tconst explicitSource = await resolveStoredRoleSource(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tconst ownershipRole = await resolveOwnershipRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\tif (ownershipRole === \"OWNER\") {\n\t\treturn \"OWNER\";\n\t}\n\n\tconst whitelist = getConnectorAdminWhitelist(runtime);\n\tconst liveMatched = matchEntityToConnectorAdminWhitelist(\n\t\toptions?.liveEntityMetadata ?? undefined,\n\t\twhitelist,\n\t);\n\n\tif (explicitRole !== \"GUEST\") {\n\t\tif (explicitRole === \"OWNER\") {\n\t\t\treturn hasConfiguredCanonicalOwner(runtime) ? \"GUEST\" : \"OWNER\";\n\t\t}\n\n\t\tif (explicitSource === \"connector_admin\") {\n\t\t\tif (Object.keys(whitelist).length === 0) {\n\t\t\t\treturn \"GUEST\";\n\t\t\t}\n\n\t\t\tif (liveMatched) {\n\t\t\t\treturn \"ADMIN\";\n\t\t\t}\n\n\t\t\tconst entityMetadata = await getEntityMetadata(runtime, entityId);\n\t\t\tconst matched = matchEntityToConnectorAdminWhitelist(\n\t\t\t\tentityMetadata,\n\t\t\t\twhitelist,\n\t\t\t);\n\t\t\tif (matched) {\n\t\t\t\treturn \"ADMIN\";\n\t\t\t}\n\n\t\t\treturn \"GUEST\";\n\t\t}\n\n\t\treturn explicitRole;\n\t}\n\n\tif (Object.keys(whitelist).length === 0) {\n\t\treturn explicitRole;\n\t}\n\n\tif (liveMatched) {\n\t\treturn \"ADMIN\";\n\t}\n\n\tconst entityMetadata = await getEntityMetadata(runtime, entityId);\n\tconst matched = matchEntityToConnectorAdminWhitelist(\n\t\tentityMetadata,\n\t\twhitelist,\n\t);\n\tif (!matched) {\n\t\treturn explicitRole;\n\t}\n\n\treturn \"ADMIN\";\n}\n\nexport async function checkSenderPrivateAccess(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<{\n\tentityId: UUID;\n\trole: RoleName;\n\tisOwner: boolean;\n\tisAdmin: boolean;\n\tcanManageRoles: boolean;\n\thasPrivateAccess: boolean;\n\taccessRole: RoleName | null;\n\taccessSource: \"owner\" | \"manual\" | \"linked_manual\" | null;\n} | null> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) return null;\n\n\tconst { world, metadata } = resolved;\n\tconst entityId = message.entityId as UUID;\n\tconst options = {\n\t\tliveEntityMetadata: getLiveEntityMetadataFromMessage(message),\n\t\tliveEntityId: entityId,\n\t};\n\tconst role = await resolveEntityRole(\n\t\truntime,\n\t\tworld,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\tconst ownershipRole = await resolveOwnershipRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\tif (ownershipRole === \"OWNER\") {\n\t\treturn {\n\t\t\tentityId,\n\t\t\trole,\n\t\t\tisOwner: true,\n\t\t\tisAdmin: true,\n\t\t\tcanManageRoles: true,\n\t\t\thasPrivateAccess: true,\n\t\t\taccessRole: \"OWNER\",\n\t\t\taccessSource: \"owner\",\n\t\t};\n\t}\n\n\tconst explicitAccess = await resolveExplicitGrantedRole(\n\t\truntime,\n\t\tmetadata,\n\t\tentityId,\n\t\toptions,\n\t);\n\n\treturn {\n\t\tentityId,\n\t\trole,\n\t\tisOwner: false,\n\t\tisAdmin: role === \"OWNER\" || role === \"ADMIN\",\n\t\tcanManageRoles: role === \"OWNER\" || role === \"ADMIN\",\n\t\thasPrivateAccess: explicitAccess !== null,\n\t\taccessRole: explicitAccess?.role ?? null,\n\t\taccessSource: explicitAccess?.source ?? null,\n\t};\n}\n\nexport function canModifyRole(\n\tactorRole: RoleName,\n\ttargetCurrentRole: RoleName,\n\tnewRole: RoleName,\n): boolean {\n\tif (targetCurrentRole === newRole) return false;\n\tconst actorRank = ROLE_RANK[actorRole];\n\tconst targetRank = ROLE_RANK[targetCurrentRole];\n\tif (actorRole === \"OWNER\") return true;\n\tif (actorRole === \"ADMIN\") {\n\t\tif (targetRank >= actorRank) return false;\n\t\tif (newRole === \"OWNER\") return false;\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport async function resolveWorldForMessage(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<{\n\tworld: Awaited<ReturnType<IAgentRuntime[\"getWorld\"]>>;\n\tmetadata: RolesWorldMetadata;\n} | null> {\n\tconst room = await runtime.getRoom(message.roomId);\n\tconst worldId =\n\t\troom?.worldId ?? resolveWorldIdFromMessageMetadata(runtime, message);\n\tif (!worldId) return null;\n\tconst world = await runtime.getWorld(worldId);\n\tif (!world) return null;\n\tconst metadata = (world.metadata ?? {}) as RolesWorldMetadata;\n\treturn { world, metadata };\n}\n\nexport async function resolveCanonicalOwnerIdForMessage(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<string | null> {\n\tconst configuredOwnerId = resolveCanonicalOwnerId(runtime);\n\tif (configuredOwnerId) {\n\t\treturn configuredOwnerId;\n\t}\n\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\treturn resolveCanonicalOwnerId(runtime, resolved?.metadata);\n}\n\nexport async function checkSenderRole(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n): Promise<RoleCheckResult | null> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) return null;\n\tconst { world, metadata } = resolved;\n\tconst entityId = message.entityId as UUID;\n\tconst role = await resolveEntityRole(runtime, world, metadata, entityId, {\n\t\tliveEntityMetadata: getLiveEntityMetadataFromMessage(message),\n\t\tliveEntityId: entityId,\n\t});\n\treturn {\n\t\tentityId,\n\t\trole,\n\t\tisOwner: role === \"OWNER\",\n\t\tisAdmin: role === \"OWNER\" || role === \"ADMIN\",\n\t\tcanManageRoles: role === \"OWNER\" || role === \"ADMIN\",\n\t};\n}\n\nexport async function setEntityRole(\n\truntime: IAgentRuntime,\n\tmessage: Memory,\n\ttargetEntityId: string,\n\tnewRole: RoleName,\n\tsource: RoleGrantSource = \"manual\",\n): Promise<Record<string, RoleName>> {\n\tconst resolved = await resolveWorldForMessage(runtime, message);\n\tif (!resolved) throw new Error(\"Cannot resolve world for role assignment\");\n\tconst { world, metadata } = resolved;\n\tif (!metadata.roles) metadata.roles = {};\n\tmetadata.roleSources ??= {};\n\tmetadata.roles[targetEntityId] = newRole;\n\tif (newRole === \"GUEST\") {\n\t\tdelete metadata.roleSources[targetEntityId];\n\t} else {\n\t\tmetadata.roleSources[targetEntityId] = source;\n\t}\n\t(world as { metadata: RolesWorldMetadata }).metadata = metadata;\n\tawait runtime.updateWorld(\n\t\tworld as Parameters<IAgentRuntime[\"updateWorld\"]>[0],\n\t);\n\treturn { ...metadata.roles };\n}\n"
|