@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.
426 lines • 15.9 kB
TypeScript
/**
* Ensemble Element - Orchestrates multiple elements working together
*
* Ensembles allow combining multiple elements (personas, skills, templates, agents, memories)
* into cohesive units with controlled activation, conflict resolution, and shared context.
*
* ARCHITECTURE:
* - Extends BaseElement for standard element behavior
* - Pure business logic (no file operations)
* - Portfolio-agnostic (PortfolioManager passed to activate())
*
* SECURITY MEASURES:
* 1. Circular dependency detection with DFS algorithm
* 2. Resource limits (max elements, nesting depth, activation time)
* 3. Input sanitization for all user-provided data
* 4. Activation timeout protection
* 5. Context size limits to prevent memory exhaustion
* 6. Audit logging for security events
* 7. Condition validation to prevent code injection
*/
import { BaseElement } from '../BaseElement.js';
import { IElement, ElementValidationResult } from '../../types/elements/index.js';
import { PortfolioManager } from '../../portfolio/PortfolioManager.js';
import { MetadataService } from '../../services/MetadataService.js';
import { EnsembleMetadata, EnsembleElement, EnsembleActivationResult, EnsembleActivationMetrics } from './types.js';
import { ResolvedEnsembleLimits } from './constants.js';
/**
* Ensemble class - Orchestrates multiple elements as a cohesive unit
*
* Extends BaseElement to inherit:
* - Standard validation
* - Serialization
* - Metadata management
* - ID generation
* - Status tracking
*/
export declare class Ensemble extends BaseElement implements IElement {
metadata: EnsembleMetadata;
private elements;
private elementInstances;
private readonly MAX_INSTANCE_CACHE_SIZE;
private instanceAccessTimes;
private sharedContext;
private activationInProgress;
private lastActivationResult?;
private activationMetrics;
private _effectiveLimits;
/**
* Get effective limits for this ensemble
*
* Resolves limits in priority order:
* 1. Per-ensemble overrides (from metadata.resourceLimits)
* 2. Global configuration (setGlobalEnsembleLimits())
* 3. Environment variables (ENSEMBLE_MAX_*)
* 4. Default values
*
* Results are cached for performance. Call invalidateLimitsCache() to refresh.
*
* @returns Resolved limits object
*/
getEffectiveLimits(): ResolvedEnsembleLimits;
/**
* Invalidate cached limits (call when resourceLimits changes)
*/
invalidateLimitsCache(): void;
constructor(metadata: Partial<EnsembleMetadata>, elements: EnsembleElement[] | undefined, metadataService: MetadataService);
/**
* Find an element by name in the elements array
* @param name - Element name to search for
* @returns Element if found, undefined otherwise
*/
private findElementByName;
/**
* Check if an element exists in the array
* @param name - Element name to check
* @returns true if element exists, false otherwise
*/
private hasElement;
/**
* Remove an element from the array by name
* @param name - Element name to remove
* @returns true if element was removed, false if not found
*/
private removeElementByName;
/**
* Add or update an element in the array
* @param element - Element to add or update
* @returns true if element was added, false if updated
*/
private setElement;
/**
* Sync elements array to metadata (single source of truth)
*/
private syncElementsToMetadata;
/**
* Sync the internal elements array from metadata.elements
*
* Use this after metadata.elements has been modified externally
* (e.g., via editElement) to ensure internal state is consistent.
*/
syncElementsFromMetadata(): void;
/**
* Load element references from metadata
* Called during construction to populate elements array
*/
private loadElementsFromMetadata;
/**
* Add an element to the ensemble
*
* @param element - Element configuration to add
* @throws Error if element would create circular dependency or exceed limits
*/
addElement(element: EnsembleElement): void;
/**
* Update an element's configuration in the ensemble
*
* @param elementName - Name of element to update
* @param updates - Partial element configuration to merge with existing
* @throws Error if element not found or activation in progress
*/
updateElement(elementName: string, updates: Partial<EnsembleElement>): void;
/**
* Remove an element from the ensemble
*
* @param elementName - Name of element to remove
* @throws Error if element not found or activation in progress
*/
removeElement(elementName: string): void;
/**
* Activate the element (BaseElement interface implementation)
* For ensembles, this is a no-op. Use activateEnsemble() for full activation.
*/
activate(): Promise<void>;
/**
* Activate the ensemble and all its elements based on strategy
*
* @param portfolioManager - Portfolio manager for loading element instances
* @param managers - Element managers for loading different element types
* @param nestingDepth - Current nesting depth (0 for top-level, increments for nested)
* @returns Activation result with success status and element results
*/
activateEnsemble(portfolioManager: PortfolioManager, managers: import('./types.js').ElementManagers, nestingDepth?: number): Promise<EnsembleActivationResult>;
/**
* Helper to create structured log context for ensemble operations
* @private
*/
private getLogContext;
/**
* Perform the actual activation logic
* Extracted to separate method for timeout handling
*/
private performActivation;
/**
* Deactivate the ensemble and all its elements
*/
deactivate(): Promise<void>;
/**
* Validate the ensemble configuration
* Overrides BaseElement.validate() to add ensemble-specific checks
*/
validate(): ElementValidationResult;
/**
* Determine activation order based on strategy
*/
private getActivationOrder;
/**
* Topological sort for dependency-based activation order
*/
private topologicalSort;
/**
* Activate elements sequentially in dependency order
*/
private activateSequential;
/**
* Activate all elements simultaneously
*/
private activateAll;
/**
* Activate elements by priority (highest first)
*/
private activatePriority;
/**
* Activate elements based on conditions
*/
private activateConditional;
/**
* Activate a single element and track results
*/
private activateSingleElement;
/**
* Activate an element via its type manager's activation method.
* This registers the element in the manager's active set so that
* get_active_elements correctly reports it.
*
* @returns true if activation went through a manager, false if no manager available
* @see Issue #1769 - Ensemble activation not registering with type managers
*/
private activateViaManager;
/**
* Load an element instance from portfolio
*/
private loadElementInstance;
/**
* Normalize element type to singular form
*/
private normalizeElementType;
/**
* Get the appropriate manager for an element type
*/
private getManagerForType;
/**
* Get list of available manager types
*/
private getAvailableManagerTypes;
/**
* Find an element in the manager using fuzzy name matching
*/
private findElementInManager;
/**
* Check if element name matches target using fuzzy matching
* Handles exact match, slugified match, and bidirectional slug matching
*
* SECURITY: Normalizes Unicode to prevent composed vs decomposed character mismatches.
* This is defense-in-depth; element names are already validated by ELEMENT_NAME_PATTERN
* which blocks non-ASCII characters.
*
* @param actualName - Name from element manager (filesystem)
* @param targetName - Name from ensemble configuration (user input)
* @returns true if names match exactly or after slugification
*/
private matchesElementName;
/**
* Capitalize first letter of a string
*/
private capitalizeFirst;
/**
* Evict the oldest accessed instance from the cache
* Implements LRU (Least Recently Used) eviction policy
*/
private evictOldestInstance;
/**
* Clear the element instance cache
* Useful for freeing memory or forcing reload of elements
*/
clearInstanceCache(): void;
/**
* Build a type-safe context for condition evaluation
*
* Constructs a ConditionContext object with all values needed to evaluate
* an element's activation condition. The context is immutable and only
* exposes safe, read-only data.
*
* SECURITY GUARANTEES:
* - Only exposes documented, safe properties
* - No access to element instances or methods
* - All nested objects are readonly (shallow immutability)
* - Context values remain type-checked as unknown
* - No exposure of internal ensemble state
*
* PERFORMANCE:
* - Minimal object allocation (reuses maps where possible)
* - O(1) lookups for element and context data
* - Lazy computation of resource metrics
* - No deep cloning (uses readonly type guards)
*
* @param element - The element whose condition is being evaluated
* @param activationStartTime - Timestamp when ensemble activation began
* @param result - Current activation result (for tracking progress)
* @returns Context builder result with context object and metadata
*
* @example
* ```typescript
* const contextResult = this.buildConditionContext(
* elementConfig,
* startTime,
* activationResult
* );
*
* // Use context in evaluation
* const shouldActivate = evaluateExpression(
* element.condition,
* contextResult.context
* );
* ```
*/
private buildConditionContext;
/**
* Evaluate an activation condition in a secure VM sandbox
*
* SECURITY IMPLEMENTATION:
* This method evaluates user-provided conditions using Node.js VM module with multiple
* layers of defense:
*
* 1. **Sandboxed Execution**: Uses vm.createContext() with frozen objects
* - No access to require, process, global, or other Node.js APIs
* - All context objects are deeply frozen (immutable)
* - No prototype chain access to prevent pollution
*
* 2. **Timeout Protection**: 100ms maximum evaluation time
* - Prevents infinite loops and ReDoS attacks
* - Terminates long-running expressions
*
* 3. **Input Validation**: Pre-validated by isValidCondition()
* - Dangerous patterns blocked (eval, Function, require, etc.)
* - Only safe operators allowed
* - Maximum length enforced
*
* 4. **Type Safety**: Enforces boolean return values
* - Non-boolean results treated as false
* - Prevents side effects from non-expression statements
*
* 5. **Error Handling**: Fail-secure principle
* - Evaluation errors result in non-activation (return false)
* - All failures logged to SecurityMonitor
* - No error details leaked to user
*
* SUPPORTED FEATURES:
* - Context variable access: context.*, element.*, state.*, resources.*, environment.*
* - Comparison operators: ==, !=, ===, !==, >, <, >=, <=
* - Logical operators: &&, ||, !
* - Parentheses for grouping
* - Array/object property access
*
* Example valid conditions:
* - "element.priority >= 80"
* - "context.security_review === true"
* - "state.activatedCount > 3 && element.role === 'primary'"
* - "context.environment === 'production' || context.override"
*
* @param condition - The condition string to evaluate (pre-validated)
* @param element - The element whose condition is being evaluated
* @param activationStartTime - When activation began (for resource tracking)
* @param result - Current activation result (for state tracking)
* @returns true if condition evaluates to true, false otherwise
* @private
* @security Multiple defense layers: sandbox, timeout, validation, immutability
*/
private evaluateCondition;
/**
* Check if a condition syntax is valid
*
* Validates that a condition:
* 1. Matches the safe character pattern (alphanumeric, operators, etc.)
* 2. Does not contain dangerous keywords or operators
* 3. Is not empty or whitespace-only
*
* SECURITY: Multi-layer validation approach
* - Layer 1: Positive pattern match (CONDITION_PATTERN) allows only safe characters
* - Layer 2: Negative pattern match (DANGEROUS_CONDITION_PATTERNS) blocks code injection
* - Layer 3: VM sandbox with timeout during evaluation (see evaluateCondition)
* This defense-in-depth strategy ensures conditions cannot execute arbitrary code
*
* @param condition - The condition string to validate
* @returns true if condition is safe, false otherwise
*/
private isValidCondition;
/**
* Check if adding an element would create a circular dependency
*/
private wouldCreateCircularDependency;
/**
* Find circular dependency path for error reporting
*/
private findCircularDependency;
/**
* Detect all circular dependencies in the ensemble
*/
private detectAllCircularDependencies;
/**
* Set a value in shared context
*
* SECURITY: Size limits prevent memory exhaustion attacks
* - Individual value size capped at MAX_CONTEXT_VALUE_SIZE (10KB)
* - Total context entries capped at MAX_CONTEXT_SIZE (1000 entries)
* - Prevents DoS via unbounded context growth
* These limits allow legitimate use while blocking malicious payloads
*
* @param key - Context key (sanitized)
* @param value - Value to store (size-limited)
* @param ownerElementName - Element setting this value (validated)
*/
setContextValue(key: string, value: unknown, ownerElementName: string): void;
/**
* Get a value from shared context
*/
getContextValue(key: string): unknown;
/**
* Clear all or specific context values
*
* @param ownerElementName - Optional: only clear values owned by this element
* @throws Error if trying to clear during activation
*/
clearContext(ownerElementName?: string): void;
/**
* Type guard to check if a value is a plain object (not null, not array)
*/
private isPlainObject;
/**
* Resolve a context conflict based on configured strategy
*/
private resolveConflict;
/**
* Update activation metrics after an activation attempt
* @private
*/
private updateActivationMetrics;
/**
* Get activation metrics for performance monitoring
* Returns a snapshot of current metrics
*/
getActivationMetrics(): Readonly<EnsembleActivationMetrics>;
/**
* Get the last activation result
*/
getLastActivationResult(): EnsembleActivationResult | undefined;
/**
* Get all elements in the ensemble
*/
getElements(): EnsembleElement[];
/**
* Get a specific element by name
*/
getElement(name: string): EnsembleElement | undefined;
}
export type { EnsembleMetadata, EnsembleElement } from './types.js';
//# sourceMappingURL=Ensemble.d.ts.map