Skip to main content
Glama
ooples

MCP Console Automation Server

AWSSSMProtocol.ts63.4 kB
import { BaseProtocol } from '../core/BaseProtocol.js'; import { ProtocolCapabilities, SessionState as BaseSessionState, } from '../core/IProtocol.js'; import { Logger } from '../utils/logger.js'; import { v4 as uuidv4 } from 'uuid'; import { WebSocket } from 'ws'; // AWS SDK v3 types and imports - made optional to handle missing dependencies interface SSMClientType { send(command: any): Promise<any>; } interface EC2ClientType { send(command: any): Promise<any>; } interface STSClientType { send(command: any): Promise<any>; } interface S3ClientType { send(command: any): Promise<any>; } interface CloudWatchLogsClientType { send(command: any): Promise<any>; } interface StartSessionCommandInput { Target: string; DocumentName?: string; Parameters?: Record<string, string[]>; } interface StartSessionCommandOutput { SessionId?: string; Token?: string; StreamUrl?: string; } interface SendCommandCommandInput { DocumentName: string; Parameters?: Record<string, string[]>; Comment?: string; TimeoutSeconds?: number; MaxConcurrency?: string; MaxErrors?: string; InstanceIds?: string[]; Targets?: Array<{ key: string; values: string[]; }>; OutputS3BucketName?: string; OutputS3KeyPrefix?: string; CloudWatchOutputConfig?: { cloudWatchLogGroupName: string; cloudWatchOutputEnabled: boolean; }; } interface LogEvent { timestamp: number; message: string; } type AwsCredentialProvider = () => Promise<{ accessKeyId: string; secretAccessKey: string; sessionToken?: string; }>; enum CommandStatus { Success = 'Success', Failed = 'Failed', Cancelled = 'Cancelled', TimedOut = 'TimedOut', InProgress = 'InProgress', Pending = 'Pending', } enum SessionStatus { Connected = 'Connected', Connecting = 'Connecting', Disconnected = 'Disconnected', Failed = 'Failed', Terminated = 'Terminated', Terminating = 'Terminating', } enum SessionState { Active = 'Active', History = 'History', } let SSMClient: any, StartSessionCommand: any, TerminateSessionCommand: any, DescribeSessionsCommand: any, ResumeSessionCommand: any, SendCommandCommand: any, ListCommandInvocationsCommand: any, GetCommandInvocationCommand: any, DescribeInstanceInformationCommand: any, GetParametersCommand: any, PutParameterCommand: any, ListDocumentsCommand: any, DescribeDocumentCommand: any, CreateDocumentCommand: any, UpdateDocumentCommand: any, DeleteDocumentCommand: any, SessionStatusValues: any, SessionStateValues: any, DocumentStatusValues: any, CommandStatusValues: any; try { const ssmModule = require('@aws-sdk/client-ssm'); SSMClient = ssmModule.SSMClient; StartSessionCommand = ssmModule.StartSessionCommand; TerminateSessionCommand = ssmModule.TerminateSessionCommand; DescribeSessionsCommand = ssmModule.DescribeSessionsCommand; ResumeSessionCommand = ssmModule.ResumeSessionCommand; SendCommandCommand = ssmModule.SendCommandCommand; ListCommandInvocationsCommand = ssmModule.ListCommandInvocationsCommand; GetCommandInvocationCommand = ssmModule.GetCommandInvocationCommand; DescribeInstanceInformationCommand = ssmModule.DescribeInstanceInformationCommand; GetParametersCommand = ssmModule.GetParametersCommand; PutParameterCommand = ssmModule.PutParameterCommand; ListDocumentsCommand = ssmModule.ListDocumentsCommand; DescribeDocumentCommand = ssmModule.DescribeDocumentCommand; CreateDocumentCommand = ssmModule.CreateDocumentCommand; UpdateDocumentCommand = ssmModule.UpdateDocumentCommand; DeleteDocumentCommand = ssmModule.DeleteDocumentCommand; SessionStatusValues = ssmModule.SessionStatus || {}; SessionStateValues = ssmModule.SessionState || {}; DocumentStatusValues = ssmModule.DocumentStatus || {}; CommandStatusValues = ssmModule.CommandStatus || {}; } catch (error) { console.warn( '@aws-sdk/client-ssm not available, AWS SSM functionality will be disabled' ); } // EC2 SDK imports - made optional let EC2Client: any, DescribeInstancesCommand: any, DescribeTagsCommand: any; try { const ec2Module = require('@aws-sdk/client-ec2'); EC2Client = ec2Module.EC2Client; DescribeInstancesCommand = ec2Module.DescribeInstancesCommand; DescribeTagsCommand = ec2Module.DescribeTagsCommand; } catch (error) { console.warn( '@aws-sdk/client-ec2 not available, EC2 functionality will be disabled' ); } // STS SDK imports - made optional let STSClient: any, AssumeRoleCommand: any, GetCallerIdentityCommand: any, GetSessionTokenCommand: any; try { const stsModule = require('@aws-sdk/client-sts'); STSClient = stsModule.STSClient; AssumeRoleCommand = stsModule.AssumeRoleCommand; GetCallerIdentityCommand = stsModule.GetCallerIdentityCommand; GetSessionTokenCommand = stsModule.GetSessionTokenCommand; } catch (error) { console.warn( '@aws-sdk/client-sts not available, STS functionality will be disabled' ); } // S3 SDK imports - made optional let S3Client: any, HeadBucketCommand: any, PutObjectCommand: any, GetObjectCommand: any; try { const s3Module = require('@aws-sdk/client-s3'); S3Client = s3Module.S3Client; HeadBucketCommand = s3Module.HeadBucketCommand; PutObjectCommand = s3Module.PutObjectCommand; GetObjectCommand = s3Module.GetObjectCommand; } catch (error) { console.warn( '@aws-sdk/client-s3 not available, S3 functionality will be disabled' ); } // CloudWatch Logs SDK imports - made optional let CloudWatchLogsClient: any, CreateLogGroupCommand: any, CreateLogStreamCommand: any, PutLogEventsCommand: any, DescribeLogGroupsCommand: any; try { const logsModule = require('@aws-sdk/client-cloudwatch-logs'); CloudWatchLogsClient = logsModule.CloudWatchLogsClient; CreateLogGroupCommand = logsModule.CreateLogGroupCommand; CreateLogStreamCommand = logsModule.CreateLogStreamCommand; PutLogEventsCommand = logsModule.PutLogEventsCommand; DescribeLogGroupsCommand = logsModule.DescribeLogGroupsCommand; } catch (error) { console.warn( '@aws-sdk/client-cloudwatch-logs not available, CloudWatch Logs functionality will be disabled' ); } // AWS Credential Providers - made optional let fromEnv: any, fromIni: any, fromInstanceMetadata: any, fromContainerMetadata: any, fromNodeProviderChain: any, fromWebToken: any; try { const credModule = require('@aws-sdk/credential-providers'); fromEnv = credModule.fromEnv; fromIni = credModule.fromIni; fromInstanceMetadata = credModule.fromInstanceMetadata; fromContainerMetadata = credModule.fromContainerMetadata; fromNodeProviderChain = credModule.fromNodeProviderChain; fromWebToken = credModule.fromWebToken; } catch (error) { console.warn( '@aws-sdk/credential-providers not available, credential provider functionality will be disabled' ); } import { AWSSSMConnectionOptions, AWSSSMSession, AWSSSMTarget, AWSSSMDocument, AWSSSMCommandExecution, AWSSSMPortForwardingSession, AWSSSMError, AWSSSMSessionLog, AWSSSMSessionManagerConfig, AWSCredentials, AWSSTSAssumedRole, AWSSSMRetryConfig, ConsoleOutput, ConsoleSession, ConsoleEvent, SessionOptions, ConsoleType, } from '../types/index.js'; import { RetryManager } from '../core/RetryManager.js'; import { ErrorRecovery } from '../core/ErrorRecovery.js'; /** * AWS Systems Manager Session Manager Protocol Implementation * * This class provides comprehensive support for AWS SSM Session Manager including: * - Interactive shell sessions * - Port forwarding tunnels * - Command execution * - Session logging to S3/CloudWatch * - Multi-region support * - IAM role assumption and credential management * - Advanced retry logic with exponential backoff * - Session recording and audit capabilities */ export class AWSSSMProtocol extends BaseProtocol { public readonly type: ConsoleType = 'aws-ssm'; public readonly capabilities: ProtocolCapabilities; private ssmClient!: SSMClientType; private ec2Client!: EC2ClientType; private stsClient!: STSClientType; private s3Client?: S3ClientType; private cloudWatchLogsClient?: CloudWatchLogsClientType; private retryManager: RetryManager; private errorRecovery: ErrorRecovery; private awsSSMSessions: Map<string, AWSSSMSession> = new Map(); private portForwardingSessions: Map<string, AWSSSMPortForwardingSession> = new Map(); private webSockets: Map<string, WebSocket> = new Map(); private config: AWSSSMSessionManagerConfig; private connectionOptions: AWSSSMConnectionOptions; private credentials?: AWSCredentials; private assumedRole?: AWSSTSAssumedRole; private sessionLogs: Map<string, AWSSSMSessionLog[]> = new Map(); private activeCommands: Map<string, AWSSSMCommandExecution> = new Map(); // Health monitoring private healthCheckInterval?: NodeJS.Timeout; private connectionHealthy: boolean = false; private lastHealthCheck: Date = new Date(); private reconnectAttempts: number = 0; constructor( options: AWSSSMConnectionOptions, config?: Partial<AWSSSMSessionManagerConfig> ) { super('AWSSSMProtocol'); this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: true, supportsAuthentication: true, supportsEncryption: true, supportsCompression: false, supportsMultiplexing: true, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: true, supportsWorkingDirectory: true, supportsSignals: true, supportsResizing: true, supportsPTY: true, maxConcurrentSessions: 10, defaultTimeout: 60000, supportedEncodings: ['utf-8'], supportedAuthMethods: ['aws-credentials', 'iam-role', 'instance-profile'], platformSupport: { windows: true, linux: true, macos: true, freebsd: false, }, }; this.connectionOptions = options; this.retryManager = new RetryManager(); this.errorRecovery = new ErrorRecovery(); // Initialize configuration with defaults this.config = this.initializeConfig(config); // Initialize AWS clients with configuration this.initializeAWSClients(); // Setup error recovery handlers this.setupErrorRecoveryHandlers(); // Start health monitoring this.startHealthMonitoring(); } /** * Initialize configuration with sensible defaults */ private initializeConfig( config?: Partial<AWSSSMSessionManagerConfig> ): AWSSSMSessionManagerConfig { return { regionConfig: { defaultRegion: this.connectionOptions.region, allowedRegions: [this.connectionOptions.region], regionPriority: [this.connectionOptions.region], }, authConfig: { credentialChain: [ 'environment', 'profile', 'iam-role', 'instance-profile', 'ecs-task-role', ], assumeRoleConfig: this.connectionOptions.roleArn ? { roleArn: this.connectionOptions.roleArn, roleSessionName: this.connectionOptions.roleSessionName || `ssm-session-${Date.now()}`, externalId: this.connectionOptions.externalId, durationSeconds: this.connectionOptions.durationSeconds || 3600, } : undefined, mfaConfig: this.connectionOptions.mfaSerial ? { mfaSerial: this.connectionOptions.mfaSerial, tokenCodeCallback: async () => this.connectionOptions.mfaTokenCode || '', } : undefined, }, sessionConfig: { defaultDocumentName: 'SSM-SessionManagerRunShell', defaultShellProfile: this.connectionOptions.shellProfile || 'bash', defaultWorkingDirectory: this.connectionOptions.workingDirectory, defaultEnvironmentVariables: this.connectionOptions.environmentVariables || {}, sessionTimeout: this.connectionOptions.sessionTimeout || 1800000, // 30 minutes maxSessionDuration: this.connectionOptions.maxSessionDuration || 7200000, // 2 hours keepAliveInterval: this.connectionOptions.keepAliveInterval || 30000, // 30 seconds maxConcurrentSessions: 10, }, loggingConfig: { enabled: !!( this.connectionOptions.s3BucketName || this.connectionOptions.cloudWatchLogGroupName ), s3Config: this.connectionOptions.s3BucketName ? { bucketName: this.connectionOptions.s3BucketName, keyPrefix: this.connectionOptions.s3KeyPrefix || 'ssm-session-logs', encryptionEnabled: this.connectionOptions.s3EncryptionEnabled !== false, } : undefined, cloudWatchConfig: this.connectionOptions.cloudWatchLogGroupName ? { logGroupName: this.connectionOptions.cloudWatchLogGroupName, encryptionEnabled: this.connectionOptions.cloudWatchEncryptionEnabled !== false, streamingEnabled: this.connectionOptions.cloudWatchStreamingEnabled !== false, } : undefined, localLogging: { enabled: true, logLevel: 'INFO', maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, }, }, connectionConfig: { connectionTimeout: this.connectionOptions.connectionTimeout || 30000, retryAttempts: this.connectionOptions.retryAttempts || 3, backoffMultiplier: this.connectionOptions.backoffMultiplier || 2, jitterEnabled: this.connectionOptions.jitterEnabled !== false, customEndpoints: this.connectionOptions.customEndpoint ? { ssm: this.connectionOptions.customEndpoint, } : undefined, useIMDSv2: this.connectionOptions.useIMDSv2 !== false, }, securityConfig: { sessionRecordingEnabled: true, complianceMode: false, encryptionInTransit: true, encryptionAtRest: true, }, monitoringConfig: { metricsEnabled: true, cloudWatchMetrics: true, customMetrics: false, healthCheckInterval: 60000, // 1 minute alertingEnabled: false, }, ...config, }; } /** * Initialize AWS SDK clients with appropriate configuration */ private initializeAWSClients(): void { const clientConfig = { region: this.connectionOptions.region, maxAttempts: this.config.connectionConfig.retryAttempts, retryMode: 'adaptive' as const, credentials: this.getCredentialProvider(), endpoint: this.config.connectionConfig.customEndpoints?.ssm, requestHandler: { connectionTimeout: this.config.connectionConfig.connectionTimeout, socketTimeout: this.config.connectionConfig.connectionTimeout * 2, }, }; if (!SSMClient) { throw new Error('@aws-sdk/client-ssm is required but not available'); } this.ssmClient = new SSMClient(clientConfig); this.ec2Client = new EC2Client(clientConfig); this.stsClient = new STSClient(clientConfig); if (this.config.loggingConfig.s3Config && S3Client) { this.s3Client = new S3Client(clientConfig); } if (this.config.loggingConfig.cloudWatchConfig && CloudWatchLogsClient) { this.cloudWatchLogsClient = new CloudWatchLogsClient(clientConfig); } } /** * Convert environment variables to SSM parameters format */ private convertEnvironmentToParameters( env: Record<string, string> ): Record<string, string[]> { const parameters: Record<string, string[]> = {}; for (const [key, value] of Object.entries(env)) { parameters[key] = [value]; } return parameters; } /** * Convert SSM parameters to environment variables format */ private convertParametersToEnvironment( params: Record<string, string[]> ): Record<string, string> { const env: Record<string, string> = {}; for (const [key, values] of Object.entries(params)) { env[key] = values[0] || ''; } return env; } /** * Get appropriate credential provider based on configuration */ private getCredentialProvider(): AwsCredentialProvider { // If explicit credentials provided if ( this.connectionOptions.accessKeyId && this.connectionOptions.secretAccessKey ) { return async () => ({ accessKeyId: this.connectionOptions.accessKeyId!, secretAccessKey: this.connectionOptions.secretAccessKey!, sessionToken: this.connectionOptions.sessionToken, }); } // If profile specified if (this.connectionOptions.profile) { return fromIni({ profile: this.connectionOptions.profile }); } // Use credential chain based on configuration const providers: AwsCredentialProvider[] = []; for (const providerType of this.config.authConfig.credentialChain) { switch (providerType) { case 'environment': providers.push(fromEnv()); break; case 'profile': providers.push(fromIni()); break; case 'instance-profile': providers.push(fromInstanceMetadata()); break; case 'ecs-task-role': providers.push(fromContainerMetadata()); break; case 'web-identity': providers.push(fromWebToken()); break; } } return fromNodeProviderChain({ providers, }); } /** * Setup error recovery event handlers */ private setupErrorRecoveryHandlers(): void { this.errorRecovery.on('recovery-attempted', (data) => { this.logger.info(`SSM error recovery attempted: ${data.strategy}`); this.emit('recovery-attempted', data); }); this.errorRecovery.on('degradation-enabled', (data) => { this.logger.warn(`SSM degraded mode enabled: ${data.reason}`); this.emit('degradation-enabled', data); }); this.errorRecovery.on('require-reauth', async (data) => { this.logger.warn('SSM re-authentication required'); await this.refreshCredentials(); this.emit('require-reauth', data); }); } /** * Start health monitoring for connections and sessions */ private startHealthMonitoring(): void { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } this.healthCheckInterval = setInterval(async () => { await this.performHealthCheck(); }, this.config.monitoringConfig.healthCheckInterval); } /** * Perform comprehensive health check */ private async performHealthCheck(): Promise<void> { try { // Test basic connectivity const identity = await this.stsClient.send( new GetCallerIdentityCommand({}) ); this.connectionHealthy = true; this.lastHealthCheck = new Date(); this.reconnectAttempts = 0; this.logger.debug( `Health check passed - Account: ${identity.Account}, User: ${identity.Arn}` ); // Check active sessions for (const [sessionId, session] of Array.from( this.awsSSMSessions.entries() )) { if (session.status === 'Connected' || session.status === 'Connecting') { await this.checkSessionHealth(sessionId); } } this.emit('health-check', { status: 'healthy', timestamp: this.lastHealthCheck, }); } catch (error) { this.connectionHealthy = false; this.reconnectAttempts++; this.logger.error( `Health check failed (attempt ${this.reconnectAttempts}):`, error ); this.emit('health-check', { status: 'unhealthy', error, timestamp: new Date(), reconnectAttempts: this.reconnectAttempts, }); // Attempt recovery if configured if ( this.reconnectAttempts <= this.config.connectionConfig.retryAttempts ) { await this.attemptReconnection({ error: 'Health check failed' }); } } } /** * Check health of a specific session */ private async checkSessionHealth(sessionId: string): Promise<void> { try { const response = await this.ssmClient.send( new DescribeSessionsCommand({ State: SessionStateValues.Active || 'Active', Filters: [ { key: 'SessionId', // Use string instead of enum since AWS SDK is optional value: sessionId, }, ], }) ); const sessionInfo = response.Sessions?.find( (s: any) => s.SessionId === sessionId ); if ( !sessionInfo || sessionInfo.Status !== (SessionStatusValues.Connected || 'Connected') ) { await this.handleSessionDisconnect(sessionId, 'Health check failed'); } } catch (error) { this.logger.error(`Session health check failed for ${sessionId}:`, error); await this.handleSessionDisconnect( sessionId, `Health check error: ${error}` ); } } /** * Attempt to reconnect and recover sessions */ protected async attemptReconnection(context: any): Promise<boolean> { try { this.logger.info( `Attempting reconnection (${this.reconnectAttempts}/${this.config.connectionConfig.retryAttempts})` ); // Refresh credentials if needed await this.refreshCredentials(); // Re-initialize clients this.initializeAWSClients(); // Attempt to reconnect active sessions for (const [sessionId, session] of Array.from( this.awsSSMSessions.entries() )) { if (session.status === 'Disconnected' || session.status === 'Failed') { await this.attemptSessionRecovery(sessionId); } } return true; // Reconnection succeeded } catch (error) { this.logger.error('Reconnection attempt failed:', error); return false; // Reconnection failed } } /** * Ensure AWS credentials are available and valid */ private async ensureCredentials(): Promise<void> { // Basic credential validation - AWS SDK will handle loading if (!this.connectionOptions.region) { throw new Error('AWS region is required'); } } /** * Refresh AWS credentials (for assume role scenarios) */ private async refreshCredentials(): Promise<void> { if (this.config.authConfig.assumeRoleConfig) { try { const assumeRoleResponse = await this.stsClient.send( new AssumeRoleCommand({ RoleArn: this.config.authConfig.assumeRoleConfig.roleArn, RoleSessionName: this.config.authConfig.assumeRoleConfig.roleSessionName, ExternalId: this.config.authConfig.assumeRoleConfig.externalId, DurationSeconds: this.config.authConfig.assumeRoleConfig.durationSeconds, Policy: this.config.authConfig.assumeRoleConfig.policy, PolicyArns: this.config.authConfig.assumeRoleConfig.policyArns?.map( (arn) => ({ arn }) ), SerialNumber: this.config.authConfig.mfaConfig?.mfaSerial, TokenCode: this.config.authConfig.mfaConfig ? await this.config.authConfig.mfaConfig.tokenCodeCallback() : undefined, }) ); if (assumeRoleResponse.Credentials) { this.credentials = { accessKeyId: assumeRoleResponse.Credentials.AccessKeyId!, secretAccessKey: assumeRoleResponse.Credentials.SecretAccessKey!, sessionToken: assumeRoleResponse.Credentials.SessionToken, expiration: assumeRoleResponse.Credentials.Expiration, }; this.assumedRole = { credentials: this.credentials, assumedRoleUser: assumeRoleResponse.AssumedRoleUser!, packedPolicySize: assumeRoleResponse.PackedPolicySize, sourceIdentity: assumeRoleResponse.SourceIdentity, }; this.logger.info('Credentials refreshed successfully'); } } catch (error) { this.logger.error('Failed to refresh credentials:', error); throw error; } } } /** * Start an interactive SSM session */ async startSession( options: Partial<AWSSSMConnectionOptions> = {} ): Promise<string> { const sessionOptions = { ...this.connectionOptions, ...options }; if (!sessionOptions.instanceId) { throw new Error('Instance ID is required for SSM sessions'); } const sessionId = uuidv4(); try { // Validate instance accessibility await this.validateTarget( sessionOptions.instanceId, sessionOptions.targetType || 'instance' ); // Prepare session parameters const sessionParams: StartSessionCommandInput = { Target: sessionOptions.instanceId, DocumentName: sessionOptions.documentName || this.config.sessionConfig.defaultDocumentName, Parameters: this.convertEnvironmentToParameters({ ...this.config.sessionConfig.defaultEnvironmentVariables, ...sessionOptions.environmentVariables, }), }; // Start the session const response = (await this.retryManager.executeWithRetry( async () => await this.ssmClient.send(new StartSessionCommand(sessionParams)), { sessionId, operationName: 'start_ssm_session', strategyName: 'ssm', context: { instanceId: sessionOptions.instanceId, documentName: sessionParams.DocumentName, }, } )) as StartSessionCommandOutput; // Create session object const session: AWSSSMSession = { sessionId: response.SessionId!, sessionToken: response.Token!, tokenValue: response.Token!, streamUrl: response.StreamUrl!, instanceId: sessionOptions.instanceId, targetType: sessionOptions.targetType || 'instance', documentName: sessionParams.DocumentName!, parameters: sessionParams.Parameters || {}, status: 'Connecting', creationDate: new Date(), lastAccessedDate: new Date(), owner: 'session-manager', sessionTimeout: this.config.sessionConfig.sessionTimeout, maxSessionDuration: this.config.sessionConfig.maxSessionDuration, shellProfile: sessionOptions.shellProfile, workingDirectory: sessionOptions.workingDirectory, environmentVariables: this.convertParametersToEnvironment( sessionParams.Parameters || {} ), region: this.connectionOptions.region, accountId: await this.getAccountId(), tags: sessionOptions.tags || {}, complianceInfo: { recordingEnabled: this.config.securityConfig.sessionRecordingEnabled, encryptionEnabled: this.config.securityConfig.encryptionInTransit, retentionPolicy: 'default', auditLogEnabled: this.config.loggingConfig.enabled, }, }; // Add S3 logging if configured if (this.config.loggingConfig.s3Config) { session.s3OutputLocation = { outputS3BucketName: this.config.loggingConfig.s3Config.bucketName, outputS3KeyPrefix: this.config.loggingConfig.s3Config.keyPrefix, outputS3Region: this.connectionOptions.region, }; } // Add CloudWatch logging if configured if (this.config.loggingConfig.cloudWatchConfig) { session.cloudWatchOutputConfig = { cloudWatchLogGroupName: this.config.loggingConfig.cloudWatchConfig.logGroupName, cloudWatchEncryptionEnabled: this.config.loggingConfig.cloudWatchConfig.encryptionEnabled, }; } this.awsSSMSessions.set(sessionId, session); // Initialize WebSocket connection await this.initializeWebSocketConnection(sessionId, response.StreamUrl!); // Setup session logging await this.setupSessionLogging(sessionId); // Start session monitoring this.startSessionMonitoring(sessionId); this.logger.info( `SSM session started: ${sessionId} for instance ${sessionOptions.instanceId}` ); this.emit('session-started', { sessionId, instanceId: sessionOptions.instanceId, }); return sessionId; } catch (error) { this.logger.error(`Failed to start SSM session:`, error); await this.handleSessionError(sessionId, error as Error); throw this.createSSMError(error as Error, 'StartSessionFailed', true); } } /** * Start a port forwarding session */ async startPortForwardingSession( targetId: string, portNumber: number, localPortNumber?: number ): Promise<string> { const sessionId = uuidv4(); try { // Validate target await this.validateTarget(targetId, 'instance'); const actualLocalPort = localPortNumber || portNumber; // Start port forwarding session const sessionParams: StartSessionCommandInput = { Target: targetId, DocumentName: 'AWS-StartPortForwardingSession', Parameters: { portNumber: [portNumber.toString()], localPortNumber: [actualLocalPort.toString()], }, }; const response = (await this.retryManager.executeWithRetry( async () => await this.ssmClient.send(new StartSessionCommand(sessionParams)), { sessionId, operationName: 'start_port_forwarding', strategyName: 'ssm', context: { targetId, portNumber, localPortNumber: actualLocalPort }, } )) as StartSessionCommandOutput; // Create port forwarding session object const portForwardingSession: AWSSSMPortForwardingSession = { sessionId: response.SessionId!, targetId, targetType: 'instance', portNumber, localPortNumber: actualLocalPort, protocol: 'TCP', status: 'Connecting', creationDate: new Date(), lastAccessedDate: new Date(), owner: 'session-manager', sessionToken: response.Token!, streamUrl: response.StreamUrl!, region: this.connectionOptions.region, tags: this.connectionOptions.tags || {}, }; this.portForwardingSessions.set(sessionId, portForwardingSession); // Initialize WebSocket connection for port forwarding await this.initializeWebSocketConnection( sessionId, response.StreamUrl!, true ); this.logger.info( `Port forwarding session started: ${sessionId} (${targetId}:${portNumber} -> localhost:${actualLocalPort})` ); this.emit('port-forwarding-started', { sessionId, targetId, portNumber, localPortNumber: actualLocalPort, }); return sessionId; } catch (error) { this.logger.error(`Failed to start port forwarding session:`, error); throw this.createSSMError( error as Error, 'StartPortForwardingFailed', true ); } } /** * Send command to multiple targets */ async sendCommand( documentName: string, parameters: Record<string, string[]>, targets?: AWSSSMTarget[] ): Promise<string> { const commandId = uuidv4(); try { const commandInput: SendCommandCommandInput = { DocumentName: documentName, Parameters: parameters, Comment: `Command executed via SSM Protocol - ${new Date().toISOString()}`, TimeoutSeconds: this.connectionOptions.sessionTimeout ? Math.floor(this.connectionOptions.sessionTimeout / 1000) : 3600, MaxConcurrency: '10', MaxErrors: '1', }; // Set targets if (targets && targets.length > 0) { if (targets.every((t) => t.type === 'instance')) { commandInput.InstanceIds = targets.map((t) => t.id); } else { commandInput.Targets = targets.map((t) => ({ key: t.type === 'tag' ? `tag:${t.name}` : t.type, values: [t.id], })); } } else if (this.connectionOptions.instanceId) { commandInput.InstanceIds = [this.connectionOptions.instanceId]; } // Add logging configuration if (this.config.loggingConfig.s3Config) { commandInput.OutputS3BucketName = this.config.loggingConfig.s3Config.bucketName; commandInput.OutputS3KeyPrefix = this.config.loggingConfig.s3Config.keyPrefix; } if (this.config.loggingConfig.cloudWatchConfig) { commandInput.CloudWatchOutputConfig = { cloudWatchLogGroupName: this.config.loggingConfig.cloudWatchConfig.logGroupName, cloudWatchOutputEnabled: true, }; } const response = await this.retryManager.executeWithRetry( async () => await this.ssmClient.send(new SendCommandCommand(commandInput)), { sessionId: commandId, operationName: 'send_command', strategyName: 'ssm', context: { documentName, parametersCount: Object.keys(parameters).length, }, } ); // Create command execution object const commandExecution: AWSSSMCommandExecution = { commandId: response.Command!.CommandId!, documentName, parameters, instanceIds: commandInput.InstanceIds, targets: commandInput.Targets, requestedDateTime: new Date(), status: 'Pending', statusDetails: 'Command sent successfully', outputS3Region: this.connectionOptions.region, outputS3BucketName: commandInput.OutputS3BucketName, outputS3KeyPrefix: commandInput.OutputS3KeyPrefix, maxConcurrency: commandInput.MaxConcurrency, maxErrors: commandInput.MaxErrors, timeoutSeconds: commandInput.TimeoutSeconds, cloudWatchOutputConfig: commandInput.CloudWatchOutputConfig, }; this.activeCommands.set(commandId, commandExecution); // Start monitoring command execution this.monitorCommandExecution(commandId); this.logger.info(`Command sent: ${commandId} (${documentName})`); this.emit('command-sent', { commandId, documentName }); return commandId; } catch (error) { this.logger.error(`Failed to send command:`, error); throw this.createSSMError(error as Error, 'SendCommandFailed', true); } } /** * Initialize WebSocket connection for session */ private async initializeWebSocketConnection( sessionId: string, streamUrl: string, isPortForwarding: boolean = false ): Promise<void> { try { const ws = new WebSocket(streamUrl); ws.on('open', () => { this.logger.debug( `WebSocket connection opened for session: ${sessionId}` ); if (isPortForwarding) { const session = this.portForwardingSessions.get(sessionId); if (session) { session.status = 'Connected'; this.portForwardingSessions.set(sessionId, session); } } else { const session = this.awsSSMSessions.get(sessionId); if (session) { session.status = 'Connected'; this.awsSSMSessions.set(sessionId, session); } } this.emit('websocket-connected', { sessionId, isPortForwarding }); }); ws.on('message', (data: Buffer) => { this.handleWebSocketMessage(sessionId, data, isPortForwarding); }); ws.on('close', (code: number, reason: Buffer) => { this.logger.debug( `WebSocket closed for session ${sessionId}: ${code} - ${reason.toString()}` ); this.handleWebSocketClose( sessionId, code, reason.toString(), isPortForwarding ); }); ws.on('error', (error: Error) => { this.logger.error(`WebSocket error for session ${sessionId}:`, error); this.handleWebSocketError(sessionId, error, isPortForwarding); }); this.webSockets.set(sessionId, ws); } catch (error) { this.logger.error( `Failed to initialize WebSocket for session ${sessionId}:`, error ); throw error; } } /** * Handle WebSocket messages */ private async handleWebSocketMessage( sessionId: string, data: Buffer, isPortForwarding: boolean ): Promise<void> { try { const message = data.toString('utf8'); // Create console output event const output: ConsoleOutput = { sessionId, type: 'stdout', data: message, timestamp: new Date(), raw: data.toString('base64'), }; // Log session data await this.logSessionData(sessionId, message, 'stdout'); // Update session statistics this.updateSessionStatistics(sessionId, data.length, 'in'); // Emit output event this.emit('output', output); this.emit('data', { sessionId, data: message, type: 'stdout' }); } catch (error) { this.logger.error( `Error handling WebSocket message for session ${sessionId}:`, error ); } } /** * Handle WebSocket close events */ private async handleWebSocketClose( sessionId: string, code: number, reason: string, isPortForwarding: boolean ): Promise<void> { if (isPortForwarding) { const session = this.portForwardingSessions.get(sessionId); if (session) { session.status = 'Disconnected'; this.portForwardingSessions.set(sessionId, session); } } else { await this.handleSessionDisconnect( sessionId, `WebSocket closed: ${code} - ${reason}` ); } this.webSockets.delete(sessionId); this.emit('websocket-closed', { sessionId, code, reason, isPortForwarding, }); } /** * Handle WebSocket errors */ private async handleWebSocketError( sessionId: string, error: Error, isPortForwarding: boolean ): Promise<void> { if (isPortForwarding) { const session = this.portForwardingSessions.get(sessionId); if (session) { session.status = 'Failed'; session.reason = error.message; this.portForwardingSessions.set(sessionId, session); } } else { await this.handleSessionError(sessionId, error); } this.emit('websocket-error', { sessionId, error, isPortForwarding }); } /** * Send input to a session */ async sendInput(sessionId: string, input: string): Promise<void> { const ws = this.webSockets.get(sessionId); if (!ws || ws.readyState !== WebSocket.OPEN) { throw new Error(`Session ${sessionId} is not connected`); } try { // Send input to WebSocket ws.send(Buffer.from(input, 'utf8')); // Log session data await this.logSessionData(sessionId, input, 'stdin'); // Update session statistics this.updateSessionStatistics(sessionId, Buffer.byteLength(input), 'out'); this.emit('input-sent', { sessionId, input }); } catch (error) { this.logger.error(`Failed to send input to session ${sessionId}:`, error); throw error; } } /** * Terminate a session */ async terminateSession(sessionId: string): Promise<void> { try { const session = this.awsSSMSessions.get(sessionId) || this.portForwardingSessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Terminate the session via AWS API await this.ssmClient.send( new TerminateSessionCommand({ SessionId: sessionId, }) ); // Close WebSocket connection const ws = this.webSockets.get(sessionId); if (ws) { ws.close(); this.webSockets.delete(sessionId); } // Update session status if (this.awsSSMSessions.has(sessionId)) { const ssmSession = this.awsSSMSessions.get(sessionId)!; ssmSession.status = 'Terminated'; this.awsSSMSessions.set(sessionId, ssmSession); } else { const portSession = this.portForwardingSessions.get(sessionId)!; portSession.status = 'Terminated'; this.portForwardingSessions.set(sessionId, portSession); } // Finalize session logging await this.finalizeSessionLogging(sessionId); this.logger.info(`Session terminated: ${sessionId}`); this.emit('session-terminated', { sessionId }); } catch (error) { this.logger.error(`Failed to terminate session ${sessionId}:`, error); throw error; } } /** * Validate target accessibility */ private async validateTarget( targetId: string, targetType: string ): Promise<void> { try { if (targetType === 'instance') { // Check if instance exists and is SSM-managed const instanceInfo = await this.ssmClient.send( new DescribeInstanceInformationCommand({ InstanceInformationFilterList: [ { key: 'InstanceIds', valueSet: [targetId], }, ], }) ); if ( !instanceInfo.InstanceInformationList || instanceInfo.InstanceInformationList.length === 0 ) { throw new Error( `Instance ${targetId} is not managed by SSM or does not exist` ); } const instance = instanceInfo.InstanceInformationList[0]; if (instance.PingStatus !== 'Online') { throw new Error( `Instance ${targetId} is not online (status: ${instance.PingStatus})` ); } } } catch (error) { this.logger.error(`Target validation failed for ${targetId}:`, error); throw error; } } /** * Setup session logging */ private async setupSessionLogging(sessionId: string): Promise<void> { if (!this.config.loggingConfig.enabled) { return; } try { // Initialize session log array this.sessionLogs.set(sessionId, []); // Create CloudWatch log stream if configured if ( this.config.loggingConfig.cloudWatchConfig && this.cloudWatchLogsClient ) { const logStreamName = `ssm-session-${sessionId}`; try { await this.cloudWatchLogsClient.send( new CreateLogStreamCommand({ logGroupName: this.config.loggingConfig.cloudWatchConfig.logGroupName, logStreamName, }) ); } catch (error: any) { // Log stream might already exist, which is fine if (error.name !== 'ResourceAlreadyExistsException') { throw error; } } } // Log session start event await this.logSessionEvent( sessionId, 'SessionStart', 'system', 'Session started' ); } catch (error) { this.logger.error( `Failed to setup session logging for ${sessionId}:`, error ); } } /** * Log session data */ private async logSessionData( sessionId: string, data: string, dataType: 'stdin' | 'stdout' | 'stderr' ): Promise<void> { if (!this.config.loggingConfig.enabled) { return; } const sessionLog: AWSSSMSessionLog = { sessionId, timestamp: new Date(), eventType: 'DataStreamEvent', source: dataType === 'stdin' ? 'client' : 'target', data, dataType, byteCount: Buffer.byteLength(data), sequenceNumber: this.getNextSequenceNumber(sessionId), accountId: await this.getAccountId(), region: this.connectionOptions.region, sessionOwner: 'session-manager', targetId: this.awsSSMSessions.get(sessionId)?.instanceId, targetType: this.awsSSMSessions.get(sessionId)?.targetType, }; // Add to in-memory log const logs = this.sessionLogs.get(sessionId) || []; logs.push(sessionLog); this.sessionLogs.set(sessionId, logs); // Send to CloudWatch Logs if configured if ( this.config.loggingConfig.cloudWatchConfig?.streamingEnabled && this.cloudWatchLogsClient ) { await this.sendToCloudWatchLogs(sessionId, sessionLog); } } /** * Log session events */ private async logSessionEvent( sessionId: string, eventType: AWSSSMSessionLog['eventType'], source: AWSSSMSessionLog['source'], data: string ): Promise<void> { if (!this.config.loggingConfig.enabled) { return; } const sessionLog: AWSSSMSessionLog = { sessionId, timestamp: new Date(), eventType, source, data, dataType: 'system', byteCount: Buffer.byteLength(data), sequenceNumber: this.getNextSequenceNumber(sessionId), accountId: await this.getAccountId(), region: this.connectionOptions.region, sessionOwner: 'session-manager', targetId: this.awsSSMSessions.get(sessionId)?.instanceId, targetType: this.awsSSMSessions.get(sessionId)?.targetType, }; // Add to in-memory log const logs = this.sessionLogs.get(sessionId) || []; logs.push(sessionLog); this.sessionLogs.set(sessionId, logs); // Send to CloudWatch Logs if configured if ( this.config.loggingConfig.cloudWatchConfig && this.cloudWatchLogsClient ) { await this.sendToCloudWatchLogs(sessionId, sessionLog); } } /** * Send log entry to CloudWatch Logs */ private async sendToCloudWatchLogs( sessionId: string, logEntry: AWSSSMSessionLog ): Promise<void> { if ( !this.cloudWatchLogsClient || !this.config.loggingConfig.cloudWatchConfig ) { return; } try { const logEvent: LogEvent = { timestamp: logEntry.timestamp.getTime(), message: JSON.stringify(logEntry), }; await this.cloudWatchLogsClient.send( new PutLogEventsCommand({ logGroupName: this.config.loggingConfig.cloudWatchConfig.logGroupName, logStreamName: `ssm-session-${sessionId}`, logEvents: [logEvent], }) ); } catch (error) { this.logger.error( `Failed to send log to CloudWatch for session ${sessionId}:`, error ); } } /** * Finalize session logging */ private async finalizeSessionLogging(sessionId: string): Promise<void> { if (!this.config.loggingConfig.enabled) { return; } try { // Log session end event await this.logSessionEvent( sessionId, 'SessionEnd', 'system', 'Session ended' ); // Upload to S3 if configured if (this.config.loggingConfig.s3Config && this.s3Client) { const logs = this.sessionLogs.get(sessionId) || []; const logData = JSON.stringify(logs, null, 2); const key = `${this.config.loggingConfig.s3Config.keyPrefix}/${sessionId}/${new Date().toISOString()}.json`; await this.s3Client.send( new PutObjectCommand({ Bucket: this.config.loggingConfig.s3Config.bucketName, Key: key, Body: logData, ContentType: 'application/json', ServerSideEncryption: this.config.loggingConfig.s3Config .encryptionEnabled ? 'AES256' : undefined, Metadata: { sessionId, region: this.connectionOptions.region, timestamp: new Date().toISOString(), }, }) ); this.logger.info( `Session logs uploaded to S3: s3://${this.config.loggingConfig.s3Config.bucketName}/${key}` ); } // Clean up in-memory logs this.sessionLogs.delete(sessionId); } catch (error) { this.logger.error( `Failed to finalize session logging for ${sessionId}:`, error ); } } /** * Get next sequence number for session logs */ private getNextSequenceNumber(sessionId: string): number { const logs = this.sessionLogs.get(sessionId) || []; return logs.length + 1; } /** * Update session statistics */ private updateSessionStatistics( sessionId: string, bytes: number, direction: 'in' | 'out' ): void { const session = this.awsSSMSessions.get(sessionId); if (!session) return; if (!session.statistics) { session.statistics = { bytesIn: 0, bytesOut: 0, packetsIn: 0, packetsOut: 0, commandsExecuted: 0, errorsCount: 0, lastActivityTime: new Date(), }; } if (direction === 'in') { session.statistics.bytesIn += bytes; session.statistics.packetsIn += 1; } else { session.statistics.bytesOut += bytes; session.statistics.packetsOut += 1; } session.statistics.lastActivityTime = new Date(); session.lastAccessedDate = new Date(); this.awsSSMSessions.set(sessionId, session); } /** * Handle session disconnect */ private async handleSessionDisconnect( sessionId: string, reason: string ): Promise<void> { const session = this.awsSSMSessions.get(sessionId); if (!session) return; session.status = 'Disconnected'; session.reason = reason; this.awsSSMSessions.set(sessionId, session); // Close WebSocket if still open const ws = this.webSockets.get(sessionId); if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } this.logger.warn(`Session ${sessionId} disconnected: ${reason}`); this.emit('session-disconnected', { sessionId, reason }); } /** * Handle session errors */ private async handleSessionError( sessionId: string, error: Error ): Promise<void> { const session = this.awsSSMSessions.get(sessionId); if (session) { session.status = 'Failed'; session.reason = error.message; this.awsSSMSessions.set(sessionId, session); } // Log error event await this.logSessionEvent( sessionId, 'ErrorEvent', 'system', `Error: ${error.message}` ); this.logger.error(`Session ${sessionId} error:`, error); this.emit('session-error', { sessionId, error }); } /** * Attempt session recovery */ private async attemptSessionRecovery(sessionId: string): Promise<void> { try { const session = this.awsSSMSessions.get(sessionId); if (!session) return; this.logger.info(`Attempting recovery for session: ${sessionId}`); // Try to resume the session await this.ssmClient.send( new ResumeSessionCommand({ SessionId: sessionId, }) ); session.status = 'Connected'; this.awsSSMSessions.set(sessionId, session); this.logger.info(`Session recovery successful: ${sessionId}`); this.emit('session-recovered', { sessionId }); } catch (error) { this.logger.error(`Session recovery failed for ${sessionId}:`, error); await this.handleSessionError(sessionId, error as Error); } } /** * Start session monitoring */ private startSessionMonitoring(sessionId: string): void { // Check session status periodically const monitorInterval = setInterval(async () => { const session = this.awsSSMSessions.get(sessionId); if (!session || session.status === 'Terminated') { clearInterval(monitorInterval); return; } try { await this.checkSessionHealth(sessionId); } catch (error) { this.logger.error(`Session monitoring error for ${sessionId}:`, error); } }, this.config.sessionConfig.keepAliveInterval); } /** * Monitor command execution */ private monitorCommandExecution(commandId: string): void { const checkStatus = async () => { try { const response = await this.ssmClient.send( new ListCommandInvocationsCommand({ CommandId: commandId, }) ); const command = this.activeCommands.get(commandId); if (!command || !response.CommandInvocations) return; // Update command status based on invocations const invocations = response.CommandInvocations; const statuses = invocations.map( (inv: any) => inv.Status as keyof typeof CommandStatus ); if (statuses.every((status: any) => status === CommandStatus.Success)) { command.status = 'Success'; } else if ( statuses.some((status: any) => status === CommandStatus.Failed) ) { command.status = 'Failed'; } else if ( statuses.some((status: any) => status === CommandStatus.Cancelled) ) { command.status = 'Cancelled'; } else if ( statuses.some((status: any) => status === CommandStatus.TimedOut) ) { command.status = 'TimedOut'; } else if ( statuses.some((status: any) => status === CommandStatus.InProgress) ) { command.status = 'InProgress'; } this.activeCommands.set(commandId, command); if ( ['Success', 'Failed', 'Cancelled', 'TimedOut'].includes( command.status ) ) { this.emit('command-completed', { commandId, status: command.status }); // Stop monitoring return; } // Continue monitoring setTimeout(checkStatus, 5000); } catch (error) { this.logger.error(`Command monitoring error for ${commandId}:`, error); } }; // Start monitoring after a short delay setTimeout(checkStatus, 2000); } /** * Get AWS account ID */ private async getAccountId(): Promise<string> { try { const identity = await this.stsClient.send( new GetCallerIdentityCommand({}) ); return identity.Account!; } catch (error) { this.logger.error('Failed to get account ID:', error); return 'unknown'; } } /** * Create standardized SSM error */ private createSSMError( originalError: Error, code: string, retryable: boolean ): AWSSSMError { return { code, message: originalError.message, retryable, originalError, region: this.connectionOptions.region, time: new Date(), }; } /** * Get session information */ getSession(sessionId: string): AWSSSMSession | undefined { return this.awsSSMSessions.get(sessionId); } /** * Get port forwarding session information */ getPortForwardingSession( sessionId: string ): AWSSSMPortForwardingSession | undefined { return this.portForwardingSessions.get(sessionId); } /** * List all active sessions */ listSessions(): AWSSSMSession[] { return Array.from(this.awsSSMSessions.values()); } /** * List all active port forwarding sessions */ listPortForwardingSessions(): AWSSSMPortForwardingSession[] { return Array.from(this.portForwardingSessions.values()); } /** * Get session logs */ getSessionLogs(sessionId: string): AWSSSMSessionLog[] { return this.sessionLogs.get(sessionId) || []; } /** * Get command execution status */ getCommandExecution(commandId: string): AWSSSMCommandExecution | undefined { return this.activeCommands.get(commandId); } /** * Check if protocol is healthy */ isHealthy(): boolean { return this.connectionHealthy; } /** * Get protocol configuration */ getConfig(): AWSSSMSessionManagerConfig { return { ...this.config }; } /** * Update protocol configuration */ updateConfig(newConfig: Partial<AWSSSMSessionManagerConfig>): void { this.config = { ...this.config, ...newConfig }; // Update health check interval if changed if (newConfig.monitoringConfig?.healthCheckInterval) { this.startHealthMonitoring(); } this.emit('config-updated', this.config); } /** * Clean up and destroy the protocol instance */ async destroy(): Promise<void> { try { // Stop health monitoring if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = undefined; } // Terminate all active sessions const terminationPromises: Promise<void>[] = []; for (const sessionId of Array.from(this.awsSSMSessions.keys())) { terminationPromises.push( this.terminateSession(sessionId).catch((err) => this.logger.error( `Failed to terminate session ${sessionId} during cleanup:`, err ) ) ); } for (const sessionId of Array.from(this.portForwardingSessions.keys())) { terminationPromises.push( this.terminateSession(sessionId).catch((err) => this.logger.error( `Failed to terminate port forwarding session ${sessionId} during cleanup:`, err ) ) ); } await Promise.allSettled(terminationPromises); // Close all WebSocket connections for (const [sessionId, ws] of Array.from(this.webSockets.entries())) { try { ws.close(); } catch (error) { this.logger.error( `Failed to close WebSocket for session ${sessionId}:`, error ); } } // Clear all collections this.awsSSMSessions.clear(); this.portForwardingSessions.clear(); this.webSockets.clear(); this.sessionLogs.clear(); this.activeCommands.clear(); this.logger.info('AWS SSM Protocol destroyed successfully'); this.emit('destroyed'); } catch (error) { this.logger.error('Error during protocol destruction:', error); throw error; } } // ========================================== // BaseProtocol Integration Methods // ========================================== async initialize(): Promise<void> { if (this.isInitialized) return; try { // Validate configuration and credentials await this.ensureCredentials(); // Setup authentication if (this.connectionOptions.roleArn) { await this.refreshCredentials(); } this.isInitialized = true; this.logger.info( 'AWS SSM protocol initialized with session management fixes' ); } catch (error: any) { this.logger.error('Failed to initialize AWS SSM protocol', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const sessionId = `aws-ssm-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; // Use session management fixes from BaseProtocol return await this.createSessionWithTypeDetection(sessionId, options); } protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: BaseSessionState ): Promise<ConsoleSession> { try { if (!options.awsSSMOptions) { throw new Error('AWS SSM options are required'); } // Create AWS SSM session using existing method const awsSessionId = await this.startSession({ instanceId: options.awsSSMOptions.instanceId, documentName: options.awsSSMOptions.documentName || 'AWS-StartSSHSession', parameters: options.awsSSMOptions.parameters, }); const consoleSession: ConsoleSession = { id: sessionId, command: options.command || 'sh', args: options.args || [], cwd: options.cwd || '/home/ssm-user', env: options.env || {}, createdAt: new Date(), status: sessionState.isOneShot ? 'initializing' : 'running', type: this.type, streaming: options.streaming ?? false, awsSSMOptions: options.awsSSMOptions, executionState: 'idle', activeCommands: new Map(), lastActivity: new Date(), pid: undefined, // AWS SSM sessions don't have local PIDs }; this.sessions.set(sessionId, consoleSession); this.outputBuffers.set(sessionId, []); this.logger.info( `AWS SSM session ${sessionId} created (${sessionState.isOneShot ? 'one-shot' : 'persistent'})` ); return consoleSession; } catch (error) { this.logger.error(`Failed to create AWS SSM session: ${error}`); // Cleanup on failure const awsSession = this.awsSSMSessions.get(sessionId); if (awsSession) { this.awsSSMSessions.delete(sessionId); } throw error; } } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const awsSession = this.awsSSMSessions.get(sessionId); const sessionState = await this.getSessionState(sessionId); if (!awsSession) { throw new Error(`AWS SSM session ${sessionId} not found`); } try { // For one-shot sessions, ensure connection first if (sessionState.isOneShot && awsSession.status !== 'Connected') { // Session should already be connected through doCreateSession // This is a fallback await this.initialize(); } // Build full command const fullCommand = args ? `${command} ${args.join(' ')}` : command; // Send command via existing method await this.sendInput(sessionId, fullCommand + '\n'); // For one-shot sessions, mark as complete when command is sent if (sessionState.isOneShot) { setTimeout(() => { this.markSessionComplete(sessionId, 0); }, 1000); // Give time for output to be captured } this.emit('commandExecuted', { sessionId, command: fullCommand, timestamp: new Date(), }); } catch (error) { this.logger.error(`Failed to execute AWS SSM command: ${error}`); throw error; } } async closeSession(sessionId: string): Promise<void> { try { // Use existing AWS SSM session termination await this.terminateSession(sessionId); // Remove from base class tracking this.sessions.delete(sessionId); this.outputBuffers.delete(sessionId); this.emit('sessionClosed', sessionId); this.logger.info('Session closed', { sessionId }); } catch (error: any) { this.logger.error('Failed to close session', { sessionId, error }); throw error; } } async dispose(): Promise<void> { this.logger.info('Disposing AWS SSM protocol'); // Close all sessions const sessionIds = Array.from(this.awsSSMSessions.keys()); for (const sessionId of sessionIds) { try { await this.closeSession(sessionId); } catch (error) { this.logger.warn( `Error closing session ${sessionId} during dispose:`, error ); } } // Use existing destroy method await this.destroy(); await this.cleanup(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server