UNPKG

@dollhousemcp/mcp-server

Version:

DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.

338 lines 13.4 kB
/** * Install AI customization elements from collection with source priority support * Supports all element types: personas, skills, templates, agents, memories, ensembles * * FEATURE (Issue #1447): Added source priority support - checks local → GitHub → collection * in priority order during installation to prevent duplicate installations. * * SECURITY FIX (2025-08-12): Fixed critical vulnerability where content was written to disk * BEFORE validation was complete. This could allow malicious content to persist on the * filesystem even when validation failed. The fix implements: * * 1. VALIDATE-BEFORE-WRITE PATTERN: All content validation (ContentValidator.sanitizePersonaContent, * SecureYamlParser.safeMatter, metadata validation, etc.) is now performed BEFORE any disk operations. * * 2. ATOMIC FILE OPERATIONS: Uses temporary file + atomic rename to prevent partial file corruption * and ensure complete cleanup on any failure during the write process. * * 3. GUARANTEED CLEANUP: If any part of the write operation fails, temporary files are automatically * cleaned up, preventing orphaned malicious content on the filesystem. * * The vulnerability existed in installContent() where fs.writeFile() was called after validation * but before final success confirmation, creating a window where malicious content could persist. */ import { GitHubClient } from './GitHubClient.js'; import { IElementMetadata } from '../types/elements/IElement.js'; import { PortfolioManager, ElementType } from '../portfolio/PortfolioManager.js'; import { ElementSource } from '../config/sourcePriority.js'; import { UnifiedIndexManager } from '../portfolio/UnifiedIndexManager.js'; import { IFileOperationsService } from '../services/FileOperationsService.js'; /** * Result of an element installation operation */ export interface InstallResult { /** Whether the installation succeeded */ success: boolean; /** User-friendly message describing the result */ message: string; /** Element metadata (if installation succeeded) */ metadata?: IElementMetadata; /** Element type */ elementType?: ElementType; /** Filename of the installed element */ filename?: string; /** Source from which the element was installed */ source?: ElementSource; /** Whether the element already existed and was skipped */ alreadyExists?: boolean; } /** * Options for element installation */ export interface InstallOptions { /** Preferred source to install from (overrides priority order) */ preferredSource?: ElementSource; /** Force overwrite even if element exists locally */ force?: boolean; /** Enable fallback to next source if current source fails */ fallbackOnError?: boolean; } export declare class ElementInstaller { private githubClient; private portfolioManager; private baseUrl; private readonly sourcePriorityConfig; private readonly unifiedIndexManager; private readonly fileOperations; constructor(githubClient: GitHubClient, options: { portfolioManager: PortfolioManager; unifiedIndexManager: UnifiedIndexManager; fileOperations?: IFileOperationsService; }); /** * Install element with source priority support (Issue #1447) * * Checks sources in priority order (local → GitHub → collection by default): * 1. Checks if element already exists locally (prevents duplicate installation) * 2. If not local, attempts GitHub portfolio (if specified) * 3. Falls back to collection (existing behavior) * * @param elementName - Name of the element to install * @param elementType - Type of element (persona, skill, etc.) * @param collectionPath - Path in collection (for collection source) * @param options - Installation options * @returns InstallResult with success status and details * * @example * // Install from best available source (checks local → GitHub → collection) * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md'); * * @example * // Force install from collection even if exists locally * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md', { force: true }); * * @example * // Install from specific source * const result = await installer.installElement('creative-writer', ElementType.PERSONA, * 'library/personas/writing/creative-writer.md', * { preferredSource: ElementSource.COLLECTION }); * * REFACTORED: Reduced cognitive complexity by extracting helper methods * ENHANCEMENT (PR #1453): Added input validation for elementName * ENHANCEMENT (PR #1453): Added structured logging for debugging */ installElement(elementName: string, elementType: ElementType, collectionPath: string, options?: InstallOptions): Promise<InstallResult>; /** * Check local element existence if force is not enabled * Extracted from installElement() to reduce cognitive complexity * * @param elementName - Name of element to check * @param elementType - Type of element * @param force - Whether to skip the check * @returns InstallResult if element exists, null otherwise * @private */ private checkLocalElementIfNeeded; /** * Determine source priority order based on preferred source * Extracted from installElement() to reduce cognitive complexity * * @param preferredSource - Optional preferred source to prioritize * @returns Array of sources in priority order * @private */ private determineSourcePriority; /** * Try installing from sources in priority order * Extracted from installElement() to reduce cognitive complexity * * @param sourcePriority - Sources to try in order * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Collection path * @param fallbackOnError - Whether to continue on errors * @returns InstallResult * @throws Error if all sources fail * @private */ private trySourcesInOrder; /** * Try installing from a single source with error handling * Extracted from trySourcesInOrder() to reduce cognitive complexity * * ENHANCEMENT (PR #1453): Added detailed error context and retry suggestions * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param source - Source to try * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Collection path * @param fallbackOnError - Whether to continue on errors * @returns Object with success flag and optional error * @private */ private tryInstallFromSource; /** * Format detailed error message with context and retry suggestions * ENHANCEMENT (PR #1453): Provides helpful guidance to users when installation fails * * @param source - Source that failed * @param elementName - Name of element * @param error - Original error * @returns Formatted error message with suggestions * @private */ private formatInstallationError; /** * Create comprehensive error for when all sources fail * Extracted to reduce cognitive complexity * * ENHANCEMENT (PR #1453): Added retry suggestions when all sources fail * * @param errors - Array of errors from each source * @returns Error with formatted summary and suggestions * @private */ private createAllSourcesFailedError; /** * Install element from a specific source * * @param source - Source to install from * @param elementName - Name of element * @param elementType - Type of element * @param collectionPath - Path in collection (for collection source) * @returns InstallResult * @private */ private installFromSource; /** * Check if element exists locally * * ENHANCEMENT (PR #1453): Added optional chaining for robustness * * @param elementName - Name of element to check * @param elementType - Type of element * @returns true if element exists locally * @private */ private checkLocalElement; /** * Install element from GitHub portfolio (Issue #1447) * * Fetches element from user's GitHub dollhouse-portfolio repository. * * ENHANCEMENT (PR #1453): Added elementName validation before filename generation * ENHANCEMENT (PR #1453): Added optional chaining for robustness * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param elementName - Name of element to install * @param elementType - Type of element * @returns InstallResult * @private */ private installFromGitHub; /** * Install element from collection (refactored from installContent) * * REFACTORED: Reduced cognitive complexity by extracting validation and processing steps * ENHANCEMENT (PR #1453): Added structured logging for debugging * * @param collectionPath - Path in collection * @returns InstallResult * @private */ private installFromCollection; /** * Validate collection path and extract element type * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @returns Element type * @throws Error if path format is invalid * @private */ private validateAndExtractElementType; /** * Fetch content from collection with size validation * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @returns Decoded content string * @throws Error if fetch fails or size exceeds limits * @private */ private fetchCollectionContent; /** * Validate and sanitize collection content * Extracted from installFromCollection() to reduce cognitive complexity * * @param content - Raw content from collection * @returns Sanitized content and parsed metadata * @throws Error if validation fails * @private */ private validateCollectionContent; /** * Remove prototype-pollution keys from metadata objects. */ private sanitizeMetadata; /** * Parse YAML content with security error handling * Extracted to reduce cognitive complexity * * @param content - Content to parse * @returns Parsed matter result with metadata * @throws Error if parsing fails or security threat detected * @private */ private parseSecureYaml; /** * Validate metadata for security threats * Extracted to reduce cognitive complexity * * @param metadata - Metadata to validate * @throws Error if validation fails * @private */ private validateMetadataSecurity; /** * Prepare file path for installation * Extracted from installFromCollection() to reduce cognitive complexity * * @param sanitizedPath - Sanitized collection path * @param elementType - Element type * @returns Filename, local path, and element directory * @private */ private prepareFilePath; /** * Check if file already exists * Extracted from installFromCollection() to reduce cognitive complexity * * @param localPath - Local file path to check * @param filename - Filename for error message * @returns InstallResult if file exists, null otherwise * @private */ private checkFileExists; /** * Install AI customization element from the collection (backward compatible) * * DEPRECATED: Use installElement() for source priority support * * Automatically detects element type from path structure * * SECURITY FIX: Implements validate-before-write pattern with atomic operations * to prevent malicious content persistence on validation failure. */ installContent(inputPath: string): Promise<{ success: boolean; message: string; metadata?: IElementMetadata; elementType?: ElementType; filename?: string; }>; /** * Atomic file write operation with guaranteed cleanup on failure * * SECURITY FIX: This method ensures that file writes are atomic and any * failures during the write process will not leave partial or corrupted * files on the filesystem. Uses temporary file + rename for atomicity. * * @param destination - Final destination path for the file * @param content - Content to write to the file * @throws Error if write operation fails (with guaranteed cleanup) */ private atomicWriteFile; /** * Get ElementType from string */ private getElementTypeFromString; /** * Format installation success message */ formatInstallSuccess(metadata: IElementMetadata, filename: string, elementType: ElementType): string; } //# sourceMappingURL=ElementInstaller.d.ts.map