diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c77082..80f9730 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,12 @@ jobs: env: CI: "true" MISE_EXPERIMENTAL: "1" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AQUA_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Keep secret and dependency scanning enabled in CI; only disable the + # remaining tools that are intentionally skipped here. + MISE_DISABLE_TOOLS: "aqua:aquasecurity/trivy,grype,semgrep" steps: - name: Checkout uses: actions/checkout@v5 diff --git a/.gitignore b/.gitignore index 2d6c245..4c18cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ junit.xml .cdk.staging/ .parcel-cache/ !/.github/workflows/docs.yml +local-docs/ diff --git a/cdk/package.json b/cdk/package.json index 85a046b..7faa142 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -18,9 +18,13 @@ "@aws-cdk/mixins-preview": "2.238.0-alpha.0", "@aws-sdk/client-bedrock-agentcore": "^3.1021.0", "@aws-sdk/client-bedrock-runtime": "^3.1021.0", + "@aws-sdk/client-ec2": "^3.1021.0", + "@aws-sdk/client-ecs": "^3.1021.0", "@aws-sdk/client-dynamodb": "^3.1021.0", "@aws-sdk/client-lambda": "^3.1021.0", + "@aws-sdk/client-s3": "^3.1021.0", "@aws-sdk/client-secrets-manager": "^3.1021.0", + "@aws-sdk/client-ssm": "^3.1021.0", "@aws-sdk/lib-dynamodb": "^3.1021.0", "@aws/durable-execution-sdk-js": "^1.1.0", "aws-cdk-lib": "^2.238.0", diff --git a/cdk/src/constructs/blueprint.ts b/cdk/src/constructs/blueprint.ts index 55ce7ed..3adeb50 100644 --- a/cdk/src/constructs/blueprint.ts +++ b/cdk/src/constructs/blueprint.ts @@ -47,7 +47,7 @@ export interface BlueprintProps { * Compute strategy type. * @default 'agentcore' */ - readonly type?: 'agentcore' | 'ecs'; + readonly type?: 'agentcore' | 'ecs' | 'ec2'; /** * Override the default runtime ARN (agentcore strategy). diff --git a/cdk/src/constructs/ec2-agent-fleet.ts b/cdk/src/constructs/ec2-agent-fleet.ts new file mode 100644 index 0000000..ded688d --- /dev/null +++ b/cdk/src/constructs/ec2-agent-fleet.ts @@ -0,0 +1,221 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Duration, RemovalPolicy } from 'aws-cdk-lib'; +import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; + +export interface Ec2AgentFleetProps { + readonly vpc: ec2.IVpc; + readonly agentImageAsset: ecr_assets.DockerImageAsset; + readonly taskTable: dynamodb.ITable; + readonly taskEventsTable: dynamodb.ITable; + readonly userConcurrencyTable: dynamodb.ITable; + readonly githubTokenSecret: secretsmanager.ISecret; + readonly memoryId?: string; + readonly instanceType?: ec2.InstanceType; + readonly desiredCapacity?: number; + readonly maxCapacity?: number; +} + +export class Ec2AgentFleet extends Construct { + public readonly securityGroup: ec2.SecurityGroup; + public readonly instanceRole: iam.Role; + public readonly payloadBucket: s3.Bucket; + public readonly autoScalingGroup: autoscaling.AutoScalingGroup; + public readonly fleetTagKey: string; + public readonly fleetTagValue: string; + + constructor(scope: Construct, id: string, props: Ec2AgentFleetProps) { + super(scope, id); + + this.fleetTagKey = 'bgagent:fleet'; + this.fleetTagValue = id; + + // Security group — egress TCP 443 only + this.securityGroup = new ec2.SecurityGroup(this, 'FleetSG', { + vpc: props.vpc, + description: 'EC2 Agent Fleet - egress TCP 443 only', + allowAllOutbound: false, + }); + + this.securityGroup.addEgressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(443), + 'Allow HTTPS egress (GitHub API, AWS services)', + ); + + // S3 bucket for payload overflow + this.payloadBucket = new s3.Bucket(this, 'PayloadBucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + encryption: s3.BucketEncryption.S3_MANAGED, + enforceSSL: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + lifecycleRules: [ + { expiration: Duration.days(7) }, + ], + }); + + // CloudWatch log group + const logGroup = new logs.LogGroup(this, 'FleetLogGroup', { + retention: logs.RetentionDays.THREE_MONTHS, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // IAM Role for instances + this.instanceRole = new iam.Role(this, 'InstanceRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'), + ], + }); + + // DynamoDB read/write on task tables + props.taskTable.grantReadWriteData(this.instanceRole); + props.taskEventsTable.grantReadWriteData(this.instanceRole); + props.userConcurrencyTable.grantReadWriteData(this.instanceRole); + + // Secrets Manager read for GitHub token + props.githubTokenSecret.grantRead(this.instanceRole); + + // Bedrock model invocation + this.instanceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: ['*'], + })); + + // CloudWatch Logs write + logGroup.grantWrite(this.instanceRole); + + // ECR pull + this.instanceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ecr:GetAuthorizationToken', + ], + resources: ['*'], + })); + this.instanceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'ecr:BatchGetImage', + 'ecr:GetDownloadUrlForLayer', + ], + resources: [props.agentImageAsset.repository.repositoryArn], + })); + + // S3 read on payload bucket + this.payloadBucket.grantRead(this.instanceRole); + + // EC2 tag management on self (conditioned on fleet tag) + this.instanceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ec2:CreateTags', 'ec2:DeleteTags'], + resources: ['*'], + conditions: { + StringEquals: { + [`ec2:ResourceTag/${this.fleetTagKey}`]: this.fleetTagValue, + }, + }, + })); + + const imageUri = props.agentImageAsset.imageUri; + + // User data: install Docker, pull image, tag as idle + const userData = ec2.UserData.forLinux(); + userData.addCommands( + '#!/bin/bash', + 'set -euo pipefail', + '', + '# Install Docker', + 'dnf install -y docker', + 'systemctl enable docker', + 'systemctl start docker', + '', + '# ECR login and pre-pull agent image', + 'REGION=$(ec2-metadata --availability-zone | cut -d" " -f2 | sed \'s/.$//\')', + `aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin $(echo '${imageUri}' | cut -d/ -f1)`, + `docker pull '${imageUri}'`, + '', + '# Tag self as idle', + 'INSTANCE_ID=$(ec2-metadata -i | cut -d" " -f2)', + 'aws ec2 create-tags --resources "$INSTANCE_ID" --region "$REGION" --tags Key=bgagent:status,Value=idle', + ); + + // Auto Scaling Group + this.autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'ASG', { + vpc: props.vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + instanceType: props.instanceType ?? new ec2.InstanceType('m7g.xlarge'), + machineImage: ec2.MachineImage.latestAmazonLinux2023({ + cpuType: ec2.AmazonLinuxCpuType.ARM_64, + }), + role: this.instanceRole, + securityGroup: this.securityGroup, + userData, + desiredCapacity: props.desiredCapacity ?? 1, + minCapacity: props.desiredCapacity ?? 1, + maxCapacity: props.maxCapacity ?? 3, + healthCheck: autoscaling.HealthCheck.ec2(), + }); + + // Tag the ASG instances for fleet identification + // CDK auto-propagates tags from the ASG to instances + this.autoScalingGroup.node.defaultChild; + this.autoScalingGroup.addUserData(`aws ec2 create-tags --resources "$(ec2-metadata -i | cut -d' ' -f2)" --region "$(ec2-metadata --availability-zone | cut -d' ' -f2 | sed 's/.$//')" --tags Key=${this.fleetTagKey},Value=${this.fleetTagValue}`); + + NagSuppressions.addResourceSuppressions(this.instanceRole, [ + { + id: 'AwsSolutions-IAM4', + reason: 'AmazonSSMManagedInstanceCore is the AWS-recommended managed policy for SSM-managed instances', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'DynamoDB index/* wildcards generated by CDK grantReadWriteData; Bedrock InvokeModel requires * resource; Secrets Manager wildcards from CDK grantRead; CloudWatch Logs wildcards from CDK grantWrite; ECR GetAuthorizationToken requires * resource; EC2 CreateTags/DeleteTags conditioned on fleet tag; S3 read wildcards from CDK grantRead', + }, + ], true); + + NagSuppressions.addResourceSuppressions(this.autoScalingGroup, [ + { + id: 'AwsSolutions-AS3', + reason: 'ASG scaling notifications are not required for this dev/preview compute backend', + }, + { + id: 'AwsSolutions-EC26', + reason: 'EBS encryption uses default AWS-managed key — sufficient for agent ephemeral workloads', + }, + ], true); + + NagSuppressions.addResourceSuppressions(this.payloadBucket, [ + { + id: 'AwsSolutions-S1', + reason: 'Server access logging not required for ephemeral payload overflow bucket with 7-day lifecycle', + }, + ], true); + } +} diff --git a/cdk/src/constructs/ecs-agent-cluster.ts b/cdk/src/constructs/ecs-agent-cluster.ts new file mode 100644 index 0000000..285d9aa --- /dev/null +++ b/cdk/src/constructs/ecs-agent-cluster.ts @@ -0,0 +1,156 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { RemovalPolicy } from 'aws-cdk-lib'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; + +export interface EcsAgentClusterProps { + readonly vpc: ec2.IVpc; + readonly agentImageAsset: ecr_assets.DockerImageAsset; + readonly taskTable: dynamodb.ITable; + readonly taskEventsTable: dynamodb.ITable; + readonly userConcurrencyTable: dynamodb.ITable; + readonly githubTokenSecret: secretsmanager.ISecret; + readonly memoryId?: string; +} + +export class EcsAgentCluster extends Construct { + public readonly cluster: ecs.Cluster; + public readonly taskDefinition: ecs.FargateTaskDefinition; + public readonly securityGroup: ec2.SecurityGroup; + public readonly containerName: string; + public readonly taskRoleArn: string; + public readonly executionRoleArn: string; + + constructor(scope: Construct, id: string, props: EcsAgentClusterProps) { + super(scope, id); + + this.containerName = 'AgentContainer'; + + // ECS Cluster with Fargate capacity provider and container insights + this.cluster = new ecs.Cluster(this, 'Cluster', { + vpc: props.vpc, + containerInsights: true, + }); + + // Security group — egress TCP 443 only + this.securityGroup = new ec2.SecurityGroup(this, 'TaskSG', { + vpc: props.vpc, + description: 'ECS Agent Tasks - egress TCP 443 only', + allowAllOutbound: false, + }); + + this.securityGroup.addEgressRule( + ec2.Peer.anyIpv4(), + ec2.Port.tcp(443), + 'Allow HTTPS egress (GitHub API, AWS services)', + ); + + // CloudWatch log group for agent task output + const logGroup = new logs.LogGroup(this, 'TaskLogGroup', { + retention: logs.RetentionDays.THREE_MONTHS, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Task execution role (used by ECS agent to pull images, write logs) + // CDK creates this automatically via taskDefinition, but we need to + // grant additional permissions to the task role. + + // Fargate task definition + this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + cpu: 2048, + memoryLimitMiB: 4096, + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.ARM64, + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + }, + }); + + // Container + this.taskDefinition.addContainer(this.containerName, { + image: ecs.ContainerImage.fromDockerImageAsset(props.agentImageAsset), + logging: ecs.LogDrivers.awsLogs({ + logGroup, + streamPrefix: 'agent', + }), + environment: { + CLAUDE_CODE_USE_BEDROCK: '1', + TASK_TABLE_NAME: props.taskTable.tableName, + TASK_EVENTS_TABLE_NAME: props.taskEventsTable.tableName, + USER_CONCURRENCY_TABLE_NAME: props.userConcurrencyTable.tableName, + LOG_GROUP_NAME: logGroup.logGroupName, + GITHUB_TOKEN_SECRET_ARN: props.githubTokenSecret.secretArn, + ...(props.memoryId && { MEMORY_ID: props.memoryId }), + }, + }); + + // Task role permissions + const taskRole = this.taskDefinition.taskRole; + + // DynamoDB read/write on task tables + props.taskTable.grantReadWriteData(taskRole); + props.taskEventsTable.grantReadWriteData(taskRole); + props.userConcurrencyTable.grantReadWriteData(taskRole); + + // Secrets Manager read for GitHub token + props.githubTokenSecret.grantRead(taskRole); + + // Bedrock model invocation + taskRole.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + resources: ['*'], + })); + + // CloudWatch Logs write + logGroup.grantWrite(taskRole); + + // Expose role ARNs for scoped iam:PassRole in the orchestrator + this.taskRoleArn = taskRole.roleArn; + this.executionRoleArn = this.taskDefinition.executionRole!.roleArn; + + NagSuppressions.addResourceSuppressions(this.taskDefinition, [ + { + id: 'AwsSolutions-IAM5', + reason: 'DynamoDB index/* wildcards generated by CDK grantReadWriteData; Bedrock InvokeModel requires * resource; Secrets Manager wildcards from CDK grantRead; CloudWatch Logs wildcards from CDK grantWrite', + }, + { + id: 'AwsSolutions-ECS2', + reason: 'Environment variables contain table names and configuration, not secrets — GitHub token is fetched from Secrets Manager at runtime', + }, + ], true); + + NagSuppressions.addResourceSuppressions(this.cluster, [ + { + id: 'AwsSolutions-ECS4', + reason: 'Container insights is enabled via the containerInsights prop', + }, + ], true); + } +} diff --git a/cdk/src/constructs/task-api.ts b/cdk/src/constructs/task-api.ts index 4d60278..159851f 100644 --- a/cdk/src/constructs/task-api.ts +++ b/cdk/src/constructs/task-api.ts @@ -100,6 +100,20 @@ export interface TaskApiProps { * First ARN is also passed as `RUNTIME_ARN` when the task record has no `agent_runtime_arn`. */ readonly agentCoreStopSessionRuntimeArns?: string[]; + + /** + * ECS cluster ARN for cancel-task to stop ECS-backed tasks. + * When provided, the cancel Lambda gets `ECS_CLUSTER_ARN` env var and `ecs:StopTask` permission. + */ + readonly ecsClusterArn?: string; + + /** + * EC2 fleet configuration for cancel-task to stop EC2-backed tasks. + * When provided, the cancel Lambda gets `ssm:CancelCommand` permission. + */ + readonly ec2FleetConfig?: { + readonly instanceRoleArn: string; + }; } /** @@ -329,6 +343,9 @@ export class TaskApi extends Construct { if (stopSessionArns.length > 0) { cancelTaskEnv.RUNTIME_ARN = stopSessionArns[0]!; } + if (props.ecsClusterArn) { + cancelTaskEnv.ECS_CLUSTER_ARN = props.ecsClusterArn; + } const cancelTaskFn = new lambda.NodejsFunction(this, 'CancelTaskFn', { entry: path.join(handlersDir, 'cancel-task.ts'), @@ -363,6 +380,25 @@ export class TaskApi extends Construct { })); } + if (props.ecsClusterArn) { + cancelTaskFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['ecs:StopTask'], + resources: ['*'], + conditions: { + ArnEquals: { + 'ecs:cluster': props.ecsClusterArn, + }, + }, + })); + } + + if (props.ec2FleetConfig) { + cancelTaskFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['ssm:CancelCommand'], + resources: ['*'], + })); + } + // Repo table read for onboarding gate if (props.repoTable) { props.repoTable.grantReadData(createTaskFn); diff --git a/cdk/src/constructs/task-orchestrator.ts b/cdk/src/constructs/task-orchestrator.ts index 7da3030..1937021 100644 --- a/cdk/src/constructs/task-orchestrator.ts +++ b/cdk/src/constructs/task-orchestrator.ts @@ -18,7 +18,7 @@ */ import * as path from 'path'; -import { Duration, Stack } from 'aws-cdk-lib'; +import { Aws, Duration, Stack } from 'aws-cdk-lib'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as iam from 'aws-cdk-lib/aws-iam'; @@ -112,6 +112,33 @@ export interface TaskOrchestratorProps { * Bedrock Guardrail version. Required when guardrailId is provided. */ readonly guardrailVersion?: string; + + /** + * ECS Fargate compute strategy configuration. + * When provided, ECS-related env vars and IAM policies are added to the orchestrator. + * All fields are required — this makes the all-or-nothing constraint self-evident at the type level. + */ + readonly ecsConfig?: { + readonly clusterArn: string; + readonly taskDefinitionArn: string; + readonly subnets: string; + readonly securityGroup: string; + readonly containerName: string; + readonly taskRoleArn: string; + readonly executionRoleArn: string; + }; + + /** + * EC2 fleet compute strategy configuration. + * When provided, EC2-related env vars and IAM policies are added to the orchestrator. + */ + readonly ec2Config?: { + readonly fleetTagKey: string; + readonly fleetTagValue: string; + readonly payloadBucketName: string; + readonly ecrImageUri: string; + readonly instanceRoleArn: string; + }; } /** @@ -173,6 +200,19 @@ export class TaskOrchestrator extends Construct { ...(props.memoryId && { MEMORY_ID: props.memoryId }), ...(props.guardrailId && { GUARDRAIL_ID: props.guardrailId }), ...(props.guardrailVersion && { GUARDRAIL_VERSION: props.guardrailVersion }), + ...(props.ecsConfig && { + ECS_CLUSTER_ARN: props.ecsConfig.clusterArn, + ECS_TASK_DEFINITION_ARN: props.ecsConfig.taskDefinitionArn, + ECS_SUBNETS: props.ecsConfig.subnets, + ECS_SECURITY_GROUP: props.ecsConfig.securityGroup, + ECS_CONTAINER_NAME: props.ecsConfig.containerName, + }), + ...(props.ec2Config && { + EC2_FLEET_TAG_KEY: props.ec2Config.fleetTagKey, + EC2_FLEET_TAG_VALUE: props.ec2Config.fleetTagValue, + EC2_PAYLOAD_BUCKET: props.ec2Config.payloadBucketName, + ECR_IMAGE_URI: props.ec2Config.ecrImageUri, + }), }, bundling: { externalModules: ['@aws-sdk/*'], @@ -213,6 +253,68 @@ export class TaskOrchestrator extends Construct { resources: runtimeResources, })); + // ECS compute strategy permissions (only when ECS is configured) + if (props.ecsConfig) { + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'ecs:RunTask', + 'ecs:DescribeTasks', + 'ecs:StopTask', + ], + resources: ['*'], + conditions: { + ArnEquals: { + 'ecs:cluster': props.ecsConfig.clusterArn, + }, + }, + })); + + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: [props.ecsConfig.taskRoleArn, props.ecsConfig.executionRoleArn], + conditions: { + StringEquals: { + 'iam:PassedToService': 'ecs-tasks.amazonaws.com', + }, + }, + })); + } + + // EC2 fleet compute strategy permissions (only when EC2 is configured) + if (props.ec2Config) { + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'ec2:DescribeInstances', + 'ec2:CreateTags', + ], + resources: ['*'], + })); + + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'ssm:SendCommand', + 'ssm:GetCommandInvocation', + 'ssm:CancelCommand', + ], + resources: ['*'], + })); + + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: [`arn:${Aws.PARTITION}:s3:::${props.ec2Config.payloadBucketName}/*`], + })); + + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: [props.ec2Config.instanceRoleArn], + conditions: { + StringEquals: { + 'iam:PassedToService': 'ec2.amazonaws.com', + }, + }, + })); + } + // Per-repo Secrets Manager grants (e.g. per-repo GitHub tokens from Blueprints) for (const [index, secretArn] of (props.additionalSecretArns ?? []).entries()) { const secret = secretsmanager.Secret.fromSecretCompleteArn( @@ -264,7 +366,7 @@ export class TaskOrchestrator extends Construct { }, { id: 'AwsSolutions-IAM5', - reason: 'DynamoDB index/* wildcards generated by CDK grantReadWriteData; AgentCore runtime/* required for sub-resource invocation; Secrets Manager wildcards generated by CDK grantRead; AgentCore Memory wildcards generated by CDK grantRead/grantWrite', + reason: 'DynamoDB index/* wildcards generated by CDK grantReadWriteData; AgentCore runtime/* required for sub-resource invocation; Secrets Manager wildcards generated by CDK grantRead; AgentCore Memory wildcards generated by CDK grantRead/grantWrite; ECS RunTask/DescribeTasks/StopTask conditioned on cluster ARN; iam:PassRole scoped to ECS task/execution roles and conditioned on ecs-tasks.amazonaws.com', }, ], true); } diff --git a/cdk/src/handlers/cancel-task.ts b/cdk/src/handlers/cancel-task.ts index 10a1102..f67d949 100644 --- a/cdk/src/handlers/cancel-task.ts +++ b/cdk/src/handlers/cancel-task.ts @@ -19,6 +19,8 @@ import { BedrockAgentCoreClient, StopRuntimeSessionCommand } from '@aws-sdk/client-bedrock-agentcore'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { ECSClient, StopTaskCommand } from '@aws-sdk/client-ecs'; +import { SSMClient, CancelCommandCommand } from '@aws-sdk/client-ssm'; import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb'; import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { ulid } from 'ulid'; @@ -31,10 +33,13 @@ import { computeTtlEpoch } from './shared/validation'; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); const agentCoreClient = new BedrockAgentCoreClient({}); +const ecsClient = new ECSClient({}); +const ssmClient = new SSMClient({}); const TABLE_NAME = process.env.TASK_TABLE_NAME!; const EVENTS_TABLE_NAME = process.env.TASK_EVENTS_TABLE_NAME!; const TASK_RETENTION_DAYS = Number(process.env.TASK_RETENTION_DAYS ?? '90'); const RUNTIME_ARN = process.env.RUNTIME_ARN; +const ECS_CLUSTER_ARN = process.env.ECS_CLUSTER_ARN; /** * DELETE /v1/tasks/{task_id} — Cancel a task. @@ -107,19 +112,81 @@ export async function handler(event: APIGatewayProxyEvent): Promise = async (event, context) => { const { task_id: taskId } = event; @@ -116,26 +120,119 @@ const durableHandler: DurableExecutionHandler = asyn } }); - // Step 4: Start agent session — invoke runtime and transition to RUNNING - await context.step('start-session', async () => { + // Step 4: Start agent session — resolve compute strategy, invoke runtime, transition to RUNNING + // Returns the full SessionHandle (serializable) so ECS polling can use it in step 5. + const sessionHandle = await context.step('start-session', async () => { try { - return await startSession(task, payload, blueprintConfig); + const strategy = resolveComputeStrategy(blueprintConfig); + const handle = await strategy.startSession({ taskId, payload, blueprintConfig }); + + // Build compute metadata for the task record so cancel-task can stop the right backend + const computeMetadata: Record = handle.strategyType === 'ecs' + ? { clusterArn: handle.clusterArn, taskArn: handle.taskArn } + : handle.strategyType === 'ec2' + ? { instanceId: handle.instanceId, commandId: handle.commandId } + : { runtimeArn: handle.runtimeArn }; + + await transitionTask(taskId, TaskStatus.HYDRATING, TaskStatus.RUNNING, { + session_id: handle.sessionId, + started_at: new Date().toISOString(), + compute_type: handle.strategyType, + compute_metadata: computeMetadata, + ...(handle.strategyType === 'agentcore' && { agent_runtime_arn: handle.runtimeArn }), + }); + await emitTaskEvent(taskId, 'session_started', { + session_id: handle.sessionId, + strategy_type: handle.strategyType, + }); + + logger.info('Session started', { + task_id: taskId, + session_id: handle.sessionId, + strategy_type: handle.strategyType, + }); + + return handle; } catch (err) { await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, task.user_id, true); throw err; } }); + // Resolve the compute strategy once and reuse it across poll iterations + // instead of constructing a new instance on every cycle. + const computeStrategy = (blueprintConfig.compute_type === 'ecs' || blueprintConfig.compute_type === 'ec2') + ? resolveComputeStrategy(blueprintConfig) + : undefined; + // Step 5: Wait for agent to finish // Polls DynamoDB on each interval. The agent writes terminal status when done. // While RUNNING, the runtime updates `agent_heartbeat_at`; if that timestamp // goes stale, `pollTaskStatus` sets `sessionUnhealthy` so we fail fast instead // of waiting the full MAX_POLL_ATTEMPTS window (~8.5h) after a silent crash. // HYDRATING without transition to RUNNING is still bounded by MAX_NON_RUNNING_POLLS (~5min). + const finalPollState = await context.waitForCondition( 'await-agent-completion', async (state) => { - return pollTaskStatus(taskId, state); + const ddbState = await pollTaskStatus(taskId, state); + let consecutiveEcsPollFailures = 0; + let consecutiveEcsCompletedPolls = 0; + + // ECS compute-level crash detection: if DDB is not terminal, check ECS task status + if ( + ddbState.lastStatus && + !TERMINAL_STATUSES.includes(ddbState.lastStatus) && + computeStrategy + ) { + try { + const ecsStatus = await computeStrategy.pollSession(sessionHandle); + if (ecsStatus.status === 'failed') { + const errorMsg = 'error' in ecsStatus ? ecsStatus.error : 'ECS task failed'; + logger.warn('ECS task failed before DDB terminal write', { + task_id: taskId, + error: errorMsg, + }); + await failTask(taskId, ddbState.lastStatus, `ECS container failed: ${errorMsg}`, task.user_id, true); + return { attempts: ddbState.attempts, lastStatus: TaskStatus.FAILED }; + } + if (ecsStatus.status === 'completed') { + consecutiveEcsCompletedPolls = (state.consecutiveEcsCompletedPolls ?? 0) + 1; + if (consecutiveEcsCompletedPolls >= MAX_CONSECUTIVE_ECS_COMPLETED_POLLS) { + // ECS task exited successfully but DDB never reached terminal — the agent + // likely crashed after container exit code 0 but before writing status. + logger.error('ECS task completed but DDB never caught up — failing task', { + task_id: taskId, + consecutive_completed_polls: consecutiveEcsCompletedPolls, + }); + await failTask(taskId, ddbState.lastStatus, `ECS task exited successfully but agent never wrote terminal status after ${consecutiveEcsCompletedPolls} polls`, task.user_id, true); + return { attempts: ddbState.attempts, lastStatus: TaskStatus.FAILED }; + } + logger.warn('ECS task completed but DDB not terminal — waiting for DDB catchup', { + task_id: taskId, + consecutive_completed_polls: consecutiveEcsCompletedPolls, + }); + } + } catch (err) { + consecutiveEcsPollFailures = (state.consecutiveEcsPollFailures ?? 0) + 1; + if (consecutiveEcsPollFailures >= MAX_CONSECUTIVE_ECS_POLL_FAILURES) { + logger.error('ECS pollSession failed repeatedly — failing task', { + task_id: taskId, + consecutive_failures: consecutiveEcsPollFailures, + error: err instanceof Error ? err.message : String(err), + }); + await failTask(taskId, ddbState.lastStatus, `ECS poll failed ${consecutiveEcsPollFailures} consecutive times: ${err instanceof Error ? err.message : String(err)}`, task.user_id, true); + return { attempts: ddbState.attempts, lastStatus: TaskStatus.FAILED }; + } + logger.warn('ECS pollSession check failed (non-fatal)', { + task_id: taskId, + consecutive_failures: consecutiveEcsPollFailures, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return { ...ddbState, consecutiveEcsPollFailures, consecutiveEcsCompletedPolls }; }, { initialState: { attempts: 0 }, diff --git a/cdk/src/handlers/shared/compute-strategy.ts b/cdk/src/handlers/shared/compute-strategy.ts new file mode 100644 index 0000000..9b04b95 --- /dev/null +++ b/cdk/src/handlers/shared/compute-strategy.ts @@ -0,0 +1,60 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { BlueprintConfig, ComputeType } from './repo-config'; +import { AgentCoreComputeStrategy } from './strategies/agentcore-strategy'; +import { Ec2ComputeStrategy } from './strategies/ec2-strategy'; +import { EcsComputeStrategy } from './strategies/ecs-strategy'; + +export type SessionHandle = + | { readonly sessionId: string; readonly strategyType: 'agentcore'; readonly runtimeArn: string } + | { readonly sessionId: string; readonly strategyType: 'ecs'; readonly clusterArn: string; readonly taskArn: string } + | { readonly sessionId: string; readonly strategyType: 'ec2'; readonly instanceId: string; readonly commandId: string }; + +export type SessionStatus = + | { readonly status: 'running' } + | { readonly status: 'completed' } + | { readonly status: 'failed'; readonly error: string }; + +export interface ComputeStrategy { + readonly type: ComputeType; + startSession(input: { + taskId: string; + payload: Record; + blueprintConfig: BlueprintConfig; + }): Promise; + pollSession(handle: SessionHandle): Promise; + stopSession(handle: SessionHandle): Promise; +} + +export function resolveComputeStrategy(blueprintConfig: BlueprintConfig): ComputeStrategy { + const computeType: ComputeType = blueprintConfig.compute_type; + switch (computeType) { + case 'agentcore': + return new AgentCoreComputeStrategy(); + case 'ecs': + return new EcsComputeStrategy(); + case 'ec2': + return new Ec2ComputeStrategy(); + default: { + const _exhaustive: never = computeType; + throw new Error(`Unknown compute_type: '${_exhaustive}'`); + } + } +} diff --git a/cdk/src/handlers/shared/orchestrator.ts b/cdk/src/handlers/shared/orchestrator.ts index 1efbf02..4e7758f 100644 --- a/cdk/src/handlers/shared/orchestrator.ts +++ b/cdk/src/handlers/shared/orchestrator.ts @@ -17,11 +17,6 @@ * SOFTWARE. */ -// Task lifecycle engine: state transitions, runtime invoke, finalization. Design: docs/design/ORCHESTRATOR.md -// Tests: cdk/test/handlers/orchestrate-task.test.ts, cdk/test/constructs/task-orchestrator.test.ts - -import { randomUUID } from 'crypto'; -import { InvokeAgentRuntimeCommand, BedrockAgentCoreClient } from '@aws-sdk/client-bedrock-agentcore'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb'; import { ulid } from 'ulid'; @@ -35,7 +30,6 @@ import { computeTtlEpoch, DEFAULT_MAX_TURNS } from './validation'; import { TaskStatus, TERMINAL_STATUSES, VALID_TRANSITIONS, type TaskStatusType } from '../../constructs/task-status'; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); -const agentCoreClient = new BedrockAgentCoreClient({}); const TABLE_NAME = process.env.TASK_TABLE_NAME!; const EVENTS_TABLE_NAME = process.env.TASK_EVENTS_TABLE_NAME!; @@ -53,6 +47,10 @@ export interface PollState { readonly lastStatus?: TaskStatusType; /** True when the agent stopped sending heartbeats while still RUNNING (likely crash/OOM). */ readonly sessionUnhealthy?: boolean; + /** Consecutive ECS poll failures — escalated to error after 3. */ + readonly consecutiveEcsPollFailures?: number; + /** Consecutive polls where ECS reports completed but DDB is not terminal — escalated after 5. */ + readonly consecutiveEcsCompletedPolls?: number; } /** After RUNNING this long, we expect `agent_heartbeat_at` from the agent (if ever set). */ @@ -240,6 +238,7 @@ export async function loadBlueprintConfig(task: TaskRecord): Promise, - blueprintConfig?: BlueprintConfig, -): Promise { - // AgentCore requires runtimeSessionId >= 33 chars; UUID v4 is 36 chars. - const sessionId = randomUUID(); - const runtimeArn = blueprintConfig?.runtime_arn ?? RUNTIME_ARN; - - const command = new InvokeAgentRuntimeCommand({ - agentRuntimeArn: runtimeArn, - runtimeSessionId: sessionId, - contentType: 'application/json', - accept: 'application/json', - payload: new TextEncoder().encode(JSON.stringify({ input: payload })), - }); - - await agentCoreClient.send(command); - - await transitionTask(task.task_id, TaskStatus.HYDRATING, TaskStatus.RUNNING, { - session_id: sessionId, - started_at: new Date().toISOString(), - agent_runtime_arn: runtimeArn, - }); - await emitTaskEvent(task.task_id, 'session_started', { session_id: sessionId }); - - logger.info('Session started', { task_id: task.task_id, session_id: sessionId }); - - return sessionId; -} - /** * Poll the task record in DynamoDB to check if the agent wrote a terminal status. * Returns the updated PollState; the waitStrategy decides whether to continue. diff --git a/cdk/src/handlers/shared/repo-config.ts b/cdk/src/handlers/shared/repo-config.ts index e80b7a0..181d3c3 100644 --- a/cdk/src/handlers/shared/repo-config.ts +++ b/cdk/src/handlers/shared/repo-config.ts @@ -25,13 +25,16 @@ import { logger } from './logger'; * Per-repository configuration written by the Blueprint CDK construct * and read at runtime by the task API gate and the orchestrator. */ +export type ComputeType = 'agentcore' | 'ecs' | 'ec2'; + export interface RepoConfig { readonly repo: string; readonly status: 'active' | 'removed'; readonly onboarded_at: string; readonly updated_at: string; - readonly compute_type?: string; + readonly compute_type?: ComputeType; readonly runtime_arn?: string; + readonly instance_type?: string; readonly model_id?: string; readonly max_turns?: number; readonly max_budget_usd?: number; @@ -47,8 +50,9 @@ export interface RepoConfig { * settings with platform defaults. */ export interface BlueprintConfig { - readonly compute_type: string; + readonly compute_type: ComputeType; readonly runtime_arn: string; + readonly instance_type?: string; readonly model_id?: string; readonly max_turns?: number; readonly max_budget_usd?: number; diff --git a/cdk/src/handlers/shared/strategies/agentcore-strategy.ts b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts new file mode 100644 index 0000000..27604ad --- /dev/null +++ b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts @@ -0,0 +1,99 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { randomUUID } from 'crypto'; +import { BedrockAgentCoreClient, InvokeAgentRuntimeCommand, StopRuntimeSessionCommand } from '@aws-sdk/client-bedrock-agentcore'; +import type { ComputeStrategy, SessionHandle, SessionStatus } from '../compute-strategy'; +import { logger } from '../logger'; +import type { BlueprintConfig } from '../repo-config'; + +let sharedClient: BedrockAgentCoreClient | undefined; +function getClient(): BedrockAgentCoreClient { + if (!sharedClient) { + sharedClient = new BedrockAgentCoreClient({}); + } + return sharedClient; +} + +export class AgentCoreComputeStrategy implements ComputeStrategy { + readonly type = 'agentcore'; + + async startSession(input: { + taskId: string; + payload: Record; + blueprintConfig: BlueprintConfig; + }): Promise { + // AgentCore requires runtimeSessionId >= 33 chars; UUID v4 is 36 chars. + const sessionId = randomUUID(); + const runtimeArn = input.blueprintConfig.runtime_arn; + + const command = new InvokeAgentRuntimeCommand({ + agentRuntimeArn: runtimeArn, + runtimeSessionId: sessionId, + contentType: 'application/json', + accept: 'application/json', + payload: new TextEncoder().encode(JSON.stringify({ input: input.payload })), + }); + + await getClient().send(command); + + logger.info('AgentCore session invoked', { task_id: input.taskId, session_id: sessionId, runtime_arn: runtimeArn }); + + return { + sessionId, + strategyType: 'agentcore', + runtimeArn, + }; + } + + async pollSession(_handle: SessionHandle): Promise { + return { status: 'running' }; + } + + async stopSession(handle: SessionHandle): Promise { + if (handle.strategyType !== 'agentcore') { + throw new Error('stopSession called with non-agentcore handle'); + } + const { runtimeArn } = handle; + + try { + await getClient().send(new StopRuntimeSessionCommand({ + agentRuntimeArn: runtimeArn, + runtimeSessionId: handle.sessionId, + })); + logger.info('AgentCore session stopped', { session_id: handle.sessionId }); + } catch (err) { + const errName = err instanceof Error ? err.name : undefined; + if (errName === 'ResourceNotFoundException') { + logger.info('AgentCore session already gone', { session_id: handle.sessionId }); + } else if (errName === 'ThrottlingException' || errName === 'AccessDeniedException') { + logger.error('Failed to stop AgentCore session', { + session_id: handle.sessionId, + error_type: errName, + error: err instanceof Error ? err.message : String(err), + }); + } else { + logger.warn('Failed to stop AgentCore session (best-effort)', { + session_id: handle.sessionId, + error: err instanceof Error ? err.message : String(err), + }); + } + } + } +} diff --git a/cdk/src/handlers/shared/strategies/ec2-strategy.ts b/cdk/src/handlers/shared/strategies/ec2-strategy.ts new file mode 100644 index 0000000..8a6d8a9 --- /dev/null +++ b/cdk/src/handlers/shared/strategies/ec2-strategy.ts @@ -0,0 +1,267 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { EC2Client, DescribeInstancesCommand, CreateTagsCommand, DeleteTagsCommand } from '@aws-sdk/client-ec2'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { SSMClient, SendCommandCommand, GetCommandInvocationCommand, CancelCommandCommand } from '@aws-sdk/client-ssm'; +import type { ComputeStrategy, SessionHandle, SessionStatus } from '../compute-strategy'; +import { logger } from '../logger'; +import type { BlueprintConfig } from '../repo-config'; + +let sharedEc2Client: EC2Client | undefined; +function getEc2Client(): EC2Client { + if (!sharedEc2Client) { + sharedEc2Client = new EC2Client({}); + } + return sharedEc2Client; +} + +let sharedSsmClient: SSMClient | undefined; +function getSsmClient(): SSMClient { + if (!sharedSsmClient) { + sharedSsmClient = new SSMClient({}); + } + return sharedSsmClient; +} + +let sharedS3Client: S3Client | undefined; +function getS3Client(): S3Client { + if (!sharedS3Client) { + sharedS3Client = new S3Client({}); + } + return sharedS3Client; +} + +const EC2_FLEET_TAG_KEY = process.env.EC2_FLEET_TAG_KEY; +const EC2_FLEET_TAG_VALUE = process.env.EC2_FLEET_TAG_VALUE; +const EC2_PAYLOAD_BUCKET = process.env.EC2_PAYLOAD_BUCKET; +const ECR_IMAGE_URI = process.env.ECR_IMAGE_URI; +const EC2_CONTAINER_NAME = process.env.EC2_CONTAINER_NAME ?? 'AgentContainer'; + +export class Ec2ComputeStrategy implements ComputeStrategy { + readonly type = 'ec2'; + + async startSession(input: { + taskId: string; + payload: Record; + blueprintConfig: BlueprintConfig; + }): Promise { + if (!EC2_FLEET_TAG_KEY || !EC2_FLEET_TAG_VALUE || !EC2_PAYLOAD_BUCKET || !ECR_IMAGE_URI) { + throw new Error( + 'EC2 compute strategy requires EC2_FLEET_TAG_KEY, EC2_FLEET_TAG_VALUE, EC2_PAYLOAD_BUCKET, and ECR_IMAGE_URI environment variables', + ); + } + + const { taskId, payload, blueprintConfig } = input; + const payloadJson = JSON.stringify(payload); + + // 1. Upload payload to S3 + const payloadKey = `tasks/${taskId}/payload.json`; + await getS3Client().send(new PutObjectCommand({ + Bucket: EC2_PAYLOAD_BUCKET, + Key: payloadKey, + Body: payloadJson, + ContentType: 'application/json', + })); + + // 2. Find an idle instance + const describeResult = await getEc2Client().send(new DescribeInstancesCommand({ + Filters: [ + { Name: `tag:${EC2_FLEET_TAG_KEY}`, Values: [EC2_FLEET_TAG_VALUE] }, + { Name: 'instance-state-name', Values: ['running'] }, + { Name: 'tag:bgagent:status', Values: ['idle'] }, + ], + })); + + const instances = (describeResult.Reservations ?? []).flatMap(r => r.Instances ?? []); + if (instances.length === 0 || !instances[0]?.InstanceId) { + throw new Error('No idle EC2 instances available in fleet'); + } + + const instanceId = instances[0].InstanceId; + + // 3. Tag instance as busy + await getEc2Client().send(new CreateTagsCommand({ + Resources: [instanceId], + Tags: [ + { Key: 'bgagent:status', Value: 'busy' }, + { Key: 'bgagent:task-id', Value: taskId }, + ], + })); + + // 4. Build the boot command (mirrors ECS strategy env vars and Python boot command) + const envExports = [ + `export TASK_ID='${taskId}'`, + `export REPO_URL='${String(payload.repo_url ?? '')}'`, + ...(payload.prompt ? [`export TASK_DESCRIPTION='${String(payload.prompt).replace(/'/g, "'\\''")}'`] : []), + ...(payload.issue_number ? [`export ISSUE_NUMBER='${String(payload.issue_number)}'`] : []), + `export MAX_TURNS='${String(payload.max_turns ?? 100)}'`, + ...(payload.max_budget_usd !== undefined ? [`export MAX_BUDGET_USD='${String(payload.max_budget_usd)}'`] : []), + ...(blueprintConfig.model_id ? [`export ANTHROPIC_MODEL='${blueprintConfig.model_id}'`] : []), + ...(blueprintConfig.system_prompt_overrides ? [`export SYSTEM_PROMPT_OVERRIDES='${blueprintConfig.system_prompt_overrides.replace(/'/g, "'\\''")}'`] : []), + "export CLAUDE_CODE_USE_BEDROCK='1'", + ...(payload.github_token_secret_arn ? [`export GITHUB_TOKEN_SECRET_ARN='${String(payload.github_token_secret_arn)}'`] : []), + ...(payload.memory_id ? [`export MEMORY_ID='${String(payload.memory_id)}'`] : []), + ]; + + const bootScript = [ + '#!/bin/bash', + 'set -euo pipefail', + '', + '# Fetch payload from S3', + `aws s3 cp "s3://${EC2_PAYLOAD_BUCKET}/${payloadKey}" /tmp/payload.json`, + 'export AGENT_PAYLOAD=$(cat /tmp/payload.json)', + '', + '# Set environment variables', + ...envExports, + '', + '# ECR login and pull', + `aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $(echo '${ECR_IMAGE_URI}' | cut -d/ -f1)`, + `docker pull '${ECR_IMAGE_URI}'`, + '', + '# Run the agent container', + 'docker run --rm \\', + ' -e TASK_ID -e REPO_URL -e CLAUDE_CODE_USE_BEDROCK -e AGENT_PAYLOAD \\', + ' -e AWS_REGION -e AWS_DEFAULT_REGION \\', + ` ${payload.prompt ? '-e TASK_DESCRIPTION ' : ''}${payload.issue_number ? '-e ISSUE_NUMBER ' : ''}-e MAX_TURNS \\`, + ` ${payload.max_budget_usd !== undefined ? '-e MAX_BUDGET_USD ' : ''}${blueprintConfig.model_id ? '-e ANTHROPIC_MODEL ' : ''}${blueprintConfig.system_prompt_overrides ? '-e SYSTEM_PROMPT_OVERRIDES ' : ''}\\`, + ` ${payload.github_token_secret_arn ? '-e GITHUB_TOKEN_SECRET_ARN ' : ''}${payload.memory_id ? '-e MEMORY_ID ' : ''}\\`, + ` '${ECR_IMAGE_URI}' \\`, + ' python -c \'import json, os, sys; sys.path.insert(0, "/app"); from entrypoint import run_task; p = json.loads(os.environ["AGENT_PAYLOAD"]); r = run_task(repo_url=p.get("repo_url",""), task_description=p.get("prompt",""), issue_number=str(p.get("issue_number","")), github_token=p.get("github_token",""), anthropic_model=p.get("model_id",""), max_turns=int(p.get("max_turns",100)), max_budget_usd=p.get("max_budget_usd"), aws_region=os.environ.get("AWS_REGION",""), task_id=p.get("task_id",""), hydrated_context=p.get("hydrated_context"), system_prompt_overrides=p.get("system_prompt_overrides",""), prompt_version=p.get("prompt_version",""), memory_id=p.get("memory_id",""), task_type=p.get("task_type","new_task"), branch_name=p.get("branch_name",""), pr_number=str(p.get("pr_number",""))); sys.exit(0 if r.get("status")=="success" else 1)\'', + '', + '# Cleanup', + 'docker system prune -f', + 'rm -f /tmp/payload.json', + '', + '# Tag instance back to idle', + 'INSTANCE_ID=$(ec2-metadata -i | cut -d" " -f2)', + 'aws ec2 create-tags --resources "$INSTANCE_ID" --tags Key=bgagent:status,Value=idle', + 'aws ec2 delete-tags --resources "$INSTANCE_ID" --tags Key=bgagent:task-id', + ].join('\n'); + + // 5. Send SSM Run Command + const ssmResult = await getSsmClient().send(new SendCommandCommand({ + DocumentName: 'AWS-RunShellScript', + InstanceIds: [instanceId], + Parameters: { + commands: [bootScript], + }, + TimeoutSeconds: 32400, // 9 hours, matches orchestrator max + })); + + const commandId = ssmResult.Command?.CommandId; + if (!commandId) { + throw new Error('SSM SendCommand returned no CommandId'); + } + + logger.info('EC2 SSM command dispatched', { + task_id: taskId, + instance_id: instanceId, + command_id: commandId, + container_name: EC2_CONTAINER_NAME, + }); + + return { + sessionId: commandId, + strategyType: 'ec2', + instanceId, + commandId, + }; + } + + async pollSession(handle: SessionHandle): Promise { + if (handle.strategyType !== 'ec2') { + throw new Error('pollSession called with non-ec2 handle'); + } + const { commandId, instanceId } = handle; + + try { + const result = await getSsmClient().send(new GetCommandInvocationCommand({ + CommandId: commandId, + InstanceId: instanceId, + })); + + const status = result.Status; + + switch (status) { + case 'InProgress': + case 'Pending': + case 'Delayed': + return { status: 'running' }; + case 'Success': + return { status: 'completed' }; + case 'Failed': + case 'Cancelled': + case 'TimedOut': + case 'Cancelling': + return { status: 'failed', error: result.StatusDetails ?? `SSM command ${status}` }; + default: + // Covers any unexpected status values — treat as running to avoid + // premature failure on transient states. + return { status: 'running' }; + } + } catch (err) { + const errName = err instanceof Error ? err.name : undefined; + if (errName === 'InvocationDoesNotExist') { + return { status: 'failed', error: 'SSM command invocation not found' }; + } + throw err; + } + } + + async stopSession(handle: SessionHandle): Promise { + if (handle.strategyType !== 'ec2') { + throw new Error('stopSession called with non-ec2 handle'); + } + const { commandId, instanceId } = handle; + + try { + await getSsmClient().send(new CancelCommandCommand({ + CommandId: commandId, + InstanceIds: [instanceId], + })); + logger.info('EC2 SSM command cancelled', { command_id: commandId, instance_id: instanceId }); + } catch (err) { + const errName = err instanceof Error ? err.name : undefined; + if (errName === 'InvalidCommandId' || errName === 'InvalidInstanceId') { + logger.info('EC2 SSM command already cancelled or not found', { command_id: commandId, instance_id: instanceId }); + } else { + logger.error('Failed to cancel EC2 SSM command', { + command_id: commandId, + instance_id: instanceId, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + // Best-effort: tag instance back to idle + try { + await getEc2Client().send(new CreateTagsCommand({ + Resources: [instanceId], + Tags: [{ Key: 'bgagent:status', Value: 'idle' }], + })); + await getEc2Client().send(new DeleteTagsCommand({ + Resources: [instanceId], + Tags: [{ Key: 'bgagent:task-id' }], + })); + } catch { + // Swallow — instance may already be terminated + } + } +} diff --git a/cdk/src/handlers/shared/strategies/ecs-strategy.ts b/cdk/src/handlers/shared/strategies/ecs-strategy.ts new file mode 100644 index 0000000..5c0ad67 --- /dev/null +++ b/cdk/src/handlers/shared/strategies/ecs-strategy.ts @@ -0,0 +1,217 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { ECSClient, RunTaskCommand, DescribeTasksCommand, StopTaskCommand } from '@aws-sdk/client-ecs'; +import type { ComputeStrategy, SessionHandle, SessionStatus } from '../compute-strategy'; +import { logger } from '../logger'; +import type { BlueprintConfig } from '../repo-config'; + +let sharedClient: ECSClient | undefined; +function getClient(): ECSClient { + if (!sharedClient) { + sharedClient = new ECSClient({}); + } + return sharedClient; +} + +const ECS_CLUSTER_ARN = process.env.ECS_CLUSTER_ARN; +const ECS_TASK_DEFINITION_ARN = process.env.ECS_TASK_DEFINITION_ARN; +const ECS_SUBNETS = process.env.ECS_SUBNETS; +const ECS_SECURITY_GROUP = process.env.ECS_SECURITY_GROUP; +const ECS_CONTAINER_NAME = process.env.ECS_CONTAINER_NAME ?? 'AgentContainer'; + +export class EcsComputeStrategy implements ComputeStrategy { + readonly type = 'ecs'; + + async startSession(input: { + taskId: string; + payload: Record; + blueprintConfig: BlueprintConfig; + }): Promise { + if (!ECS_CLUSTER_ARN || !ECS_TASK_DEFINITION_ARN || !ECS_SUBNETS || !ECS_SECURITY_GROUP) { + throw new Error( + 'ECS compute strategy requires ECS_CLUSTER_ARN, ECS_TASK_DEFINITION_ARN, ECS_SUBNETS, and ECS_SECURITY_GROUP environment variables', + ); + } + + const subnets = ECS_SUBNETS.split(',').map(s => s.trim()).filter(Boolean); + const { taskId, payload, blueprintConfig } = input; + + // The ECS container's default CMD starts the FastAPI server (uvicorn) which + // waits for HTTP POST to /invocations — but in standalone ECS nobody sends + // that request. We override the container command to invoke run_task() + // directly with the full orchestrator payload (including hydrated_context). + // This avoids the server entirely and runs the agent in batch mode. + const payloadJson = JSON.stringify(payload); + + const containerEnv = [ + { name: 'TASK_ID', value: taskId }, + { name: 'REPO_URL', value: String(payload.repo_url ?? '') }, + ...(payload.prompt ? [{ name: 'TASK_DESCRIPTION', value: String(payload.prompt) }] : []), + ...(payload.issue_number ? [{ name: 'ISSUE_NUMBER', value: String(payload.issue_number) }] : []), + { name: 'MAX_TURNS', value: String(payload.max_turns ?? 100) }, + ...(payload.max_budget_usd !== undefined ? [{ name: 'MAX_BUDGET_USD', value: String(payload.max_budget_usd) }] : []), + ...(blueprintConfig.model_id ? [{ name: 'ANTHROPIC_MODEL', value: blueprintConfig.model_id }] : []), + ...(blueprintConfig.system_prompt_overrides ? [{ name: 'SYSTEM_PROMPT_OVERRIDES', value: blueprintConfig.system_prompt_overrides }] : []), + { name: 'CLAUDE_CODE_USE_BEDROCK', value: '1' }, + // Full orchestrator payload as JSON — the Python wrapper reads this to + // call run_task() with all fields including hydrated_context. + { name: 'AGENT_PAYLOAD', value: payloadJson }, + ...(payload.github_token_secret_arn + ? [{ name: 'GITHUB_TOKEN_SECRET_ARN', value: String(payload.github_token_secret_arn) }] + : []), + ...(payload.memory_id ? [{ name: 'MEMORY_ID', value: String(payload.memory_id) }] : []), + ]; + + // Override the container command to run a Python one-liner that: + // 1. Reads the AGENT_PAYLOAD env var (full orchestrator payload JSON) + // 2. Calls entrypoint.run_task() directly with all fields + // 3. Exits with code 0 on success, 1 on failure + // This bypasses the uvicorn server entirely — no HTTP, no OTEL noise. + const bootCommand = [ + 'python', '-c', + 'import json, os, sys; ' + + 'sys.path.insert(0, "/app"); ' + + 'from entrypoint import run_task; ' + + 'p = json.loads(os.environ["AGENT_PAYLOAD"]); ' + + 'r = run_task(' + + 'repo_url=p.get("repo_url",""), ' + + 'task_description=p.get("prompt",""), ' + + 'issue_number=str(p.get("issue_number","")), ' + + 'github_token=p.get("github_token",""), ' + + 'anthropic_model=p.get("model_id",""), ' + + 'max_turns=int(p.get("max_turns",100)), ' + + 'max_budget_usd=p.get("max_budget_usd"), ' + + 'aws_region=os.environ.get("AWS_REGION",""), ' + + 'task_id=p.get("task_id",""), ' + + 'hydrated_context=p.get("hydrated_context"), ' + + 'system_prompt_overrides=p.get("system_prompt_overrides",""), ' + + 'prompt_version=p.get("prompt_version",""), ' + + 'memory_id=p.get("memory_id",""), ' + + 'task_type=p.get("task_type","new_task"), ' + + 'branch_name=p.get("branch_name",""), ' + + 'pr_number=str(p.get("pr_number",""))' + + '); ' + + 'sys.exit(0 if r.get("status")=="success" else 1)', + ]; + + const command = new RunTaskCommand({ + cluster: ECS_CLUSTER_ARN, + taskDefinition: ECS_TASK_DEFINITION_ARN, + launchType: 'FARGATE', + networkConfiguration: { + awsvpcConfiguration: { + subnets, + securityGroups: [ECS_SECURITY_GROUP], + assignPublicIp: 'DISABLED', + }, + }, + overrides: { + containerOverrides: [{ + name: ECS_CONTAINER_NAME, + environment: containerEnv, + command: bootCommand, + }], + }, + }); + + const result = await getClient().send(command); + + const ecsTask = result.tasks?.[0]; + if (!ecsTask?.taskArn) { + const failures = result.failures?.map(f => `${f.arn}: ${f.reason}`).join('; ') ?? 'unknown'; + throw new Error(`ECS RunTask returned no task: ${failures}`); + } + + logger.info('ECS Fargate task started', { + task_id: taskId, + ecs_task_arn: ecsTask.taskArn, + cluster: ECS_CLUSTER_ARN, + }); + + return { + sessionId: ecsTask.taskArn, + strategyType: 'ecs', + clusterArn: ECS_CLUSTER_ARN, + taskArn: ecsTask.taskArn, + }; + } + + async pollSession(handle: SessionHandle): Promise { + if (handle.strategyType !== 'ecs') { + throw new Error('pollSession called with non-ecs handle'); + } + const { clusterArn, taskArn } = handle; + + const result = await getClient().send(new DescribeTasksCommand({ + cluster: clusterArn, + tasks: [taskArn], + })); + + const ecsTask = result.tasks?.[0]; + if (!ecsTask) { + return { status: 'failed', error: `ECS task ${taskArn} not found` }; + } + + const lastStatus = ecsTask.lastStatus; + + if (lastStatus === 'STOPPED') { + const container = ecsTask.containers?.[0]; + const exitCode = container?.exitCode; + const stoppedReason = ecsTask.stoppedReason ?? container?.reason ?? 'unknown'; + + if (exitCode === 0) { + return { status: 'completed' }; + } + if (exitCode === undefined || exitCode === null) { + return { status: 'failed', error: `Task stopped: ${stoppedReason}` }; + } + return { status: 'failed', error: `Exit code ${exitCode}: ${stoppedReason}` }; + } + + // PENDING, PROVISIONING, ACTIVATING, RUNNING, DEACTIVATING, DEPROVISIONING + return { status: 'running' }; + } + + async stopSession(handle: SessionHandle): Promise { + if (handle.strategyType !== 'ecs') { + throw new Error('stopSession called with non-ecs handle'); + } + const { clusterArn, taskArn } = handle; + + try { + await getClient().send(new StopTaskCommand({ + cluster: clusterArn, + task: taskArn, + reason: 'Stopped by orchestrator', + })); + logger.info('ECS task stopped', { task_arn: taskArn }); + } catch (err) { + const errName = err instanceof Error ? err.name : undefined; + if (errName === 'InvalidParameterException' || errName === 'ResourceNotFoundException') { + logger.info('ECS task already stopped or not found', { task_arn: taskArn }); + } else { + logger.error('Failed to stop ECS task', { + task_arn: taskArn, + error: err instanceof Error ? err.message : String(err), + }); + } + } + } +} diff --git a/cdk/src/handlers/shared/types.ts b/cdk/src/handlers/shared/types.ts index c54dda9..df7862b 100644 --- a/cdk/src/handlers/shared/types.ts +++ b/cdk/src/handlers/shared/types.ts @@ -17,6 +17,7 @@ * SOFTWARE. */ +import type { ComputeType } from './repo-config'; import type { TaskStatusType } from '../../constructs/task-status'; /** Valid task types for task creation. */ @@ -63,6 +64,8 @@ export interface TaskRecord { readonly max_budget_usd?: number; readonly prompt_version?: string; readonly memory_written?: boolean; + readonly compute_type?: ComputeType; + readonly compute_metadata?: Record; readonly ttl?: number; } diff --git a/cdk/src/stacks/agent.ts b/cdk/src/stacks/agent.ts index d7fa064..b28f6be 100644 --- a/cdk/src/stacks/agent.ts +++ b/cdk/src/stacks/agent.ts @@ -23,6 +23,8 @@ import * as bedrock from '@aws-cdk/aws-bedrock-alpha'; import * as agentcoremixins from '@aws-cdk/mixins-preview/aws-bedrockagentcore'; import { Stack, StackProps, RemovalPolicy, CfnOutput, CfnResource } from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; +// ecr_assets import is only needed when the ECS block below is uncommented +// import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; @@ -34,6 +36,8 @@ import { AgentVpc } from '../constructs/agent-vpc'; import { Blueprint } from '../constructs/blueprint'; import { ConcurrencyReconciler } from '../constructs/concurrency-reconciler'; import { DnsFirewall } from '../constructs/dns-firewall'; +// import { EcsAgentCluster } from '../constructs/ecs-agent-cluster'; +// import { Ec2AgentFleet } from '../constructs/ec2-agent-fleet'; import { RepoTable } from '../constructs/repo-table'; import { TaskApi } from '../constructs/task-api'; import { TaskDashboard } from '../constructs/task-dashboard'; @@ -273,6 +277,41 @@ export class AgentStack extends Stack { inputGuardrail.createVersion('Initial version'); + // --- ECS Fargate compute backend (optional) --- + // To enable ECS as an alternative compute backend, uncomment the block below + // and the EcsAgentCluster import at the top of this file. Repos can then use + // compute_type: 'ecs' in their blueprint config to route tasks to ECS Fargate. + // + // const agentImageAsset = new ecr_assets.DockerImageAsset(this, 'AgentImage', { + // directory: runnerPath, + // platform: ecr_assets.Platform.LINUX_ARM64, + // }); + // + // const ecsCluster = new EcsAgentCluster(this, 'EcsAgentCluster', { + // vpc: agentVpc.vpc, + // agentImageAsset, + // taskTable: taskTable.table, + // taskEventsTable: taskEventsTable.table, + // userConcurrencyTable: userConcurrencyTable.table, + // githubTokenSecret, + // memoryId: agentMemory.memory.memoryId, + // }); + + // --- EC2 fleet compute backend (optional) --- + // To enable EC2 as an alternative compute backend, uncomment the block below + // and the Ec2AgentFleet import at the top of this file. Repos can then use + // compute_type: 'ec2' in their blueprint config to route tasks to the EC2 fleet. + // + // const ec2Fleet = new Ec2AgentFleet(this, 'Ec2AgentFleet', { + // vpc: agentVpc.vpc, + // agentImageAsset, + // taskTable: taskTable.table, + // taskEventsTable: taskEventsTable.table, + // userConcurrencyTable: userConcurrencyTable.table, + // githubTokenSecret, + // memoryId: agentMemory.memory.memoryId, + // }); + // --- Task Orchestrator (durable Lambda function) --- const orchestrator = new TaskOrchestrator(this, 'TaskOrchestrator', { taskTable: taskTable.table, @@ -284,6 +323,24 @@ export class AgentStack extends Stack { memoryId: agentMemory.memory.memoryId, guardrailId: inputGuardrail.guardrailId, guardrailVersion: inputGuardrail.guardrailVersion, + // To wire ECS, uncomment the ecsCluster block above and add: + // ecsConfig: { + // clusterArn: ecsCluster.cluster.clusterArn, + // taskDefinitionArn: ecsCluster.taskDefinition.taskDefinitionArn, + // subnets: agentVpc.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds.join(','), + // securityGroup: ecsCluster.securityGroup.securityGroupId, + // containerName: ecsCluster.containerName, + // taskRoleArn: ecsCluster.taskRoleArn, + // executionRoleArn: ecsCluster.executionRoleArn, + // }, + // To wire EC2, uncomment the ec2Fleet block above and add: + // ec2Config: { + // fleetTagKey: ec2Fleet.fleetTagKey, + // fleetTagValue: ec2Fleet.fleetTagValue, + // payloadBucketName: ec2Fleet.payloadBucket.bucketName, + // ecrImageUri: agentImageAsset.imageUri, + // instanceRoleArn: ec2Fleet.instanceRole.roleArn, + // }, }); // Grant the orchestrator Lambda read+write access to memory @@ -306,6 +363,10 @@ export class AgentStack extends Stack { guardrailId: inputGuardrail.guardrailId, guardrailVersion: inputGuardrail.guardrailVersion, agentCoreStopSessionRuntimeArns: [runtime.agentRuntimeArn], + // To allow cancel-task to stop ECS-backed tasks, uncomment: + // ecsClusterArn: ecsCluster.cluster.clusterArn, + // To allow cancel-task to stop EC2-backed tasks, uncomment: + // ec2FleetConfig: { instanceRoleArn: ec2Fleet.instanceRole.roleArn }, }); // --- Operator dashboard --- diff --git a/cdk/test/constructs/ec2-agent-fleet.test.ts b/cdk/test/constructs/ec2-agent-fleet.test.ts new file mode 100644 index 0000000..30d009f --- /dev/null +++ b/cdk/test/constructs/ec2-agent-fleet.test.ts @@ -0,0 +1,213 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as path from 'path'; +import { App, Stack } from 'aws-cdk-lib'; +import { Template, Match } from 'aws-cdk-lib/assertions'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import { Ec2AgentFleet } from '../../src/constructs/ec2-agent-fleet'; + +function createStack(overrides?: { memoryId?: string }): { stack: Stack; template: Template } { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + + const agentImageAsset = new ecr_assets.DockerImageAsset(stack, 'AgentImage', { + directory: path.join(__dirname, '..', '..', '..', 'agent'), + }); + + const taskTable = new dynamodb.Table(stack, 'TaskTable', { + partitionKey: { name: 'task_id', type: dynamodb.AttributeType.STRING }, + }); + + const taskEventsTable = new dynamodb.Table(stack, 'TaskEventsTable', { + partitionKey: { name: 'task_id', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'event_id', type: dynamodb.AttributeType.STRING }, + }); + + const userConcurrencyTable = new dynamodb.Table(stack, 'UserConcurrencyTable', { + partitionKey: { name: 'user_id', type: dynamodb.AttributeType.STRING }, + }); + + const githubTokenSecret = new secretsmanager.Secret(stack, 'GitHubTokenSecret'); + + new Ec2AgentFleet(stack, 'Ec2AgentFleet', { + vpc, + agentImageAsset, + taskTable, + taskEventsTable, + userConcurrencyTable, + githubTokenSecret, + memoryId: overrides?.memoryId, + }); + + const template = Template.fromStack(stack); + return { stack, template }; +} + +describe('Ec2AgentFleet construct', () => { + test('creates an Auto Scaling Group with launch template', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::AutoScaling::AutoScalingGroup', { + MinSize: '1', + MaxSize: '3', + DesiredCapacity: '1', + }); + }); + + test('creates a security group with TCP 443 egress only', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'EC2 Agent Fleet - egress TCP 443 only', + SecurityGroupEgress: Match.arrayWith([ + Match.objectLike({ + IpProtocol: 'tcp', + FromPort: 443, + ToPort: 443, + CidrIp: '0.0.0.0/0', + }), + ]), + }); + }); + + test('creates an S3 bucket with lifecycle rule', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::S3::Bucket', { + LifecycleConfiguration: { + Rules: Match.arrayWith([ + Match.objectLike({ + ExpirationInDays: 7, + Status: 'Enabled', + }), + ]), + }, + }); + }); + + test('instance role has DynamoDB read/write permissions', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: Match.arrayWith([ + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + ]), + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('instance role has Secrets Manager read permission', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: Match.arrayWith([ + 'secretsmanager:GetSecretValue', + ]), + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('instance role has Bedrock InvokeModel permissions', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Effect: 'Allow', + Resource: '*', + }), + ]), + }, + }); + }); + + test('instance role has SSM managed policy', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Role', { + ManagedPolicyArns: Match.arrayWith([ + Match.objectLike({ + 'Fn::Join': Match.arrayWith([ + Match.arrayWith([ + Match.stringLikeRegexp('AmazonSSMManagedInstanceCore'), + ]), + ]), + }), + ]), + }); + }); + + test('creates a CloudWatch log group with 3-month retention', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 90, + }); + }); + + test('instance role has ECR pull permissions', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'ecr:GetAuthorizationToken', + Effect: 'Allow', + Resource: '*', + }), + ]), + }, + }); + }); + + test('instance role has EC2 tag management permissions conditioned on fleet tag', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: ['ec2:CreateTags', 'ec2:DeleteTags'], + Effect: 'Allow', + Condition: { + StringEquals: { + 'ec2:ResourceTag/bgagent:fleet': 'Ec2AgentFleet', + }, + }, + }), + ]), + }, + }); + }); +}); diff --git a/cdk/test/constructs/ecs-agent-cluster.test.ts b/cdk/test/constructs/ecs-agent-cluster.test.ts new file mode 100644 index 0000000..3e5854d --- /dev/null +++ b/cdk/test/constructs/ecs-agent-cluster.test.ts @@ -0,0 +1,202 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as path from 'path'; +import { App, Stack } from 'aws-cdk-lib'; +import { Template, Match } from 'aws-cdk-lib/assertions'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import { EcsAgentCluster } from '../../src/constructs/ecs-agent-cluster'; + +function createStack(overrides?: { memoryId?: string }): { stack: Stack; template: Template } { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 2 }); + + const agentImageAsset = new ecr_assets.DockerImageAsset(stack, 'AgentImage', { + directory: path.join(__dirname, '..', '..', '..', 'agent'), + }); + + const taskTable = new dynamodb.Table(stack, 'TaskTable', { + partitionKey: { name: 'task_id', type: dynamodb.AttributeType.STRING }, + }); + + const taskEventsTable = new dynamodb.Table(stack, 'TaskEventsTable', { + partitionKey: { name: 'task_id', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'event_id', type: dynamodb.AttributeType.STRING }, + }); + + const userConcurrencyTable = new dynamodb.Table(stack, 'UserConcurrencyTable', { + partitionKey: { name: 'user_id', type: dynamodb.AttributeType.STRING }, + }); + + const githubTokenSecret = new secretsmanager.Secret(stack, 'GitHubTokenSecret'); + + new EcsAgentCluster(stack, 'EcsAgentCluster', { + vpc, + agentImageAsset, + taskTable, + taskEventsTable, + userConcurrencyTable, + githubTokenSecret, + memoryId: overrides?.memoryId, + }); + + const template = Template.fromStack(stack); + return { stack, template }; +} + +describe('EcsAgentCluster construct', () => { + test('creates an ECS Cluster with container insights', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::ECS::Cluster', { + ClusterSettings: Match.arrayWith([ + Match.objectLike({ + Name: 'containerInsights', + Value: 'enabled', + }), + ]), + }); + }); + + test('creates a Fargate task definition with 2 vCPU and 4 GB', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::ECS::TaskDefinition', { + Cpu: '2048', + Memory: '4096', + RequiresCompatibilities: ['FARGATE'], + RuntimePlatform: { + CpuArchitecture: 'ARM64', + OperatingSystemFamily: 'LINUX', + }, + }); + }); + + test('creates a security group with TCP 443 egress only', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::EC2::SecurityGroup', { + GroupDescription: 'ECS Agent Tasks - egress TCP 443 only', + SecurityGroupEgress: Match.arrayWith([ + Match.objectLike({ + IpProtocol: 'tcp', + FromPort: 443, + ToPort: 443, + CidrIp: '0.0.0.0/0', + }), + ]), + }); + }); + + test('creates a CloudWatch log group with 3-month retention and CDK-generated name', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::Logs::LogGroup', { + RetentionInDays: 90, + }); + // Verify no hardcoded log group name — CDK auto-generates a unique name + const logGroups = template.findResources('AWS::Logs::LogGroup'); + for (const [, lg] of Object.entries(logGroups)) { + expect((lg as any).Properties).not.toHaveProperty('LogGroupName'); + } + }); + + test('task role has DynamoDB read/write permissions', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: Match.arrayWith([ + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + ]), + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('task role has Secrets Manager read permission', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: Match.arrayWith([ + 'secretsmanager:GetSecretValue', + ]), + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('task role has Bedrock InvokeModel permissions', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Effect: 'Allow', + Resource: '*', + }), + ]), + }, + }); + }); + + test('container has required environment variables', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: Match.arrayWith([ + Match.objectLike({ + Name: 'AgentContainer', + Environment: Match.arrayWith([ + Match.objectLike({ Name: 'CLAUDE_CODE_USE_BEDROCK', Value: '1' }), + Match.objectLike({ Name: 'TASK_TABLE_NAME', Value: Match.anyValue() }), + Match.objectLike({ Name: 'TASK_EVENTS_TABLE_NAME', Value: Match.anyValue() }), + Match.objectLike({ Name: 'USER_CONCURRENCY_TABLE_NAME', Value: Match.anyValue() }), + Match.objectLike({ Name: 'LOG_GROUP_NAME', Value: Match.anyValue() }), + ]), + }), + ]), + }); + }); + + test('includes MEMORY_ID in container env when provided', () => { + const { template } = createStack({ memoryId: 'mem-test-123' }); + template.hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: Match.arrayWith([ + Match.objectLike({ + Environment: Match.arrayWith([ + Match.objectLike({ Name: 'MEMORY_ID', Value: 'mem-test-123' }), + ]), + }), + ]), + }); + }); +}); diff --git a/cdk/test/constructs/task-api.test.ts b/cdk/test/constructs/task-api.test.ts index 862564f..ee415a8 100644 --- a/cdk/test/constructs/task-api.test.ts +++ b/cdk/test/constructs/task-api.test.ts @@ -303,6 +303,44 @@ describe('TaskApi construct', () => { }, }); }); + + test('cancelTask Lambda gets ECS_CLUSTER_ARN env var and ecs:StopTask when ecsClusterArn is set', () => { + const { template } = createStack({ + ecsClusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + }); + + // Cancel Lambda should have the ECS_CLUSTER_ARN env var + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: { + Variables: Match.objectLike({ + ECS_CLUSTER_ARN: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + }), + }, + }); + + // Should have ecs:StopTask permission + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'ecs:StopTask', + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('cancelTask Lambda does not get ECS env vars when ecsClusterArn is not set', () => { + const { template } = createStack(); + + // Find all Lambda functions and verify none have ECS_CLUSTER_ARN + const functions = template.findResources('AWS::Lambda::Function'); + for (const [, fn] of Object.entries(functions)) { + const vars = (fn as any).Properties?.Environment?.Variables ?? {}; + expect(vars).not.toHaveProperty('ECS_CLUSTER_ARN'); + } + }); }); describe('TaskApi construct with webhooks', () => { diff --git a/cdk/test/constructs/task-orchestrator.test.ts b/cdk/test/constructs/task-orchestrator.test.ts index 0b2c2ef..0612020 100644 --- a/cdk/test/constructs/task-orchestrator.test.ts +++ b/cdk/test/constructs/task-orchestrator.test.ts @@ -32,6 +32,15 @@ interface StackOverrides { memoryId?: string; guardrailId?: string; guardrailVersion?: string; + ecsConfig?: { + clusterArn: string; + taskDefinitionArn: string; + subnets: string; + securityGroup: string; + containerName: string; + taskRoleArn: string; + executionRoleArn: string; + }; } function createStack(overrides?: StackOverrides): { stack: Stack; template: Template } { @@ -57,7 +66,16 @@ function createStack(overrides?: StackOverrides): { stack: Stack; template: Temp }) : undefined; - const { includeRepoTable: _, additionalRuntimeArns, additionalSecretArns, memoryId, guardrailId, guardrailVersion, ...rest } = overrides ?? {}; + const { + includeRepoTable: _, + additionalRuntimeArns, + additionalSecretArns, + memoryId, + guardrailId, + guardrailVersion, + ecsConfig, + ...rest + } = overrides ?? {}; new TaskOrchestrator(stack, 'TaskOrchestrator', { taskTable, @@ -70,6 +88,7 @@ function createStack(overrides?: StackOverrides): { stack: Stack; template: Temp ...(memoryId && { memoryId }), ...(guardrailId && { guardrailId }), ...(guardrailVersion && { guardrailVersion }), + ...(ecsConfig && { ecsConfig }), ...rest, }); @@ -415,4 +434,103 @@ describe('TaskOrchestrator construct', () => { 'guardrailId is required when guardrailVersion is provided', ); }); + + describe('ECS compute strategy', () => { + const ecsOverrides = { + ecsConfig: { + clusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + taskDefinitionArn: 'arn:aws:ecs:us-east-1:123456789012:task-definition/agent:1', + subnets: 'subnet-aaa,subnet-bbb', + securityGroup: 'sg-12345', + containerName: 'AgentContainer', + taskRoleArn: 'arn:aws:iam::123456789012:role/TaskRole', + executionRoleArn: 'arn:aws:iam::123456789012:role/ExecutionRole', + }, + }; + + test('includes ECS env vars when ECS props are provided', () => { + const { template } = createStack(ecsOverrides); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: { + Variables: Match.objectLike({ + ECS_CLUSTER_ARN: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + ECS_TASK_DEFINITION_ARN: 'arn:aws:ecs:us-east-1:123456789012:task-definition/agent:1', + ECS_SUBNETS: 'subnet-aaa,subnet-bbb', + ECS_SECURITY_GROUP: 'sg-12345', + ECS_CONTAINER_NAME: 'AgentContainer', + }), + }, + }); + }); + + test('does not include ECS env vars when ECS props are omitted', () => { + const { template } = createStack(); + const functions = template.findResources('AWS::Lambda::Function'); + for (const [, fn] of Object.entries(functions)) { + const envVars = (fn as any).Properties.Environment?.Variables ?? {}; + expect(envVars).not.toHaveProperty('ECS_CLUSTER_ARN'); + expect(envVars).not.toHaveProperty('ECS_TASK_DEFINITION_ARN'); + } + }); + + test('grants ECS RunTask/DescribeTasks/StopTask permissions when ECS props are provided', () => { + const { template } = createStack(ecsOverrides); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: [ + 'ecs:RunTask', + 'ecs:DescribeTasks', + 'ecs:StopTask', + ], + Effect: 'Allow', + Resource: '*', + Condition: { + ArnEquals: { + 'ecs:cluster': 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + }, + }, + }), + ]), + }, + }); + }); + + test('grants iam:PassRole scoped to task/execution role ARNs', () => { + const { template } = createStack(ecsOverrides); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'iam:PassRole', + Effect: 'Allow', + Resource: Match.arrayWith([ + 'arn:aws:iam::123456789012:role/TaskRole', + 'arn:aws:iam::123456789012:role/ExecutionRole', + ]), + Condition: { + StringEquals: { + 'iam:PassedToService': 'ecs-tasks.amazonaws.com', + }, + }, + }), + ]), + }, + }); + }); + + test('does not grant ECS permissions when ECS props are omitted', () => { + const { template } = createStack(); + const policies = template.findResources('AWS::IAM::Policy'); + for (const [, policy] of Object.entries(policies)) { + const statements = (policy as any).Properties.PolicyDocument.Statement; + for (const stmt of statements) { + if (Array.isArray(stmt.Action)) { + expect(stmt.Action).not.toContain('ecs:RunTask'); + } + } + } + }); + }); }); diff --git a/cdk/test/handlers/cancel-task.test.ts b/cdk/test/handlers/cancel-task.test.ts index ca83729..68e7b1d 100644 --- a/cdk/test/handlers/cancel-task.test.ts +++ b/cdk/test/handlers/cancel-task.test.ts @@ -22,10 +22,15 @@ import type { APIGatewayProxyEvent } from 'aws-lambda'; // --- Mocks --- const mockSend = jest.fn(); const mockAgentCoreSend = jest.fn(); +const mockEcsSend = jest.fn(); jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ BedrockAgentCoreClient: jest.fn(() => ({ send: mockAgentCoreSend })), StopRuntimeSessionCommand: jest.fn((input: unknown) => ({ _type: 'StopRuntimeSession', input })), })); +jest.mock('@aws-sdk/client-ecs', () => ({ + ECSClient: jest.fn(() => ({ send: mockEcsSend })), + StopTaskCommand: jest.fn((input: unknown) => ({ _type: 'StopTask', input })), +})); jest.mock('@aws-sdk/client-dynamodb', () => ({ DynamoDBClient: jest.fn(() => ({})) })); jest.mock('@aws-sdk/lib-dynamodb', () => ({ DynamoDBDocumentClient: { from: jest.fn(() => ({ send: mockSend })) }, @@ -40,6 +45,7 @@ process.env.TASK_TABLE_NAME = 'Tasks'; process.env.TASK_EVENTS_TABLE_NAME = 'TaskEvents'; process.env.TASK_RETENTION_DAYS = '90'; process.env.RUNTIME_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/default'; +process.env.ECS_CLUSTER_ARN = 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster'; import { handler } from '../../src/handlers/cancel-task'; @@ -107,6 +113,7 @@ function makeEvent(overrides: Partial = {}): APIGatewayPro beforeEach(() => { jest.clearAllMocks(); mockAgentCoreSend.mockResolvedValue({}); + mockEcsSend.mockResolvedValue({}); // Default: GetCommand returns running task, UpdateCommand + PutCommand succeed mockSend .mockResolvedValueOnce({ Item: RUNNING_TASK }) // GetCommand @@ -274,4 +281,89 @@ describe('cancel-task handler', () => { expect(result.statusCode).toBe(200); expect(mockAgentCoreSend).toHaveBeenCalled(); }); + + test('cancels ECS-backed running task via StopTask', async () => { + mockSend.mockReset(); + const ecsTask = { + ...RUNNING_TASK, + compute_type: 'ecs', + compute_metadata: { + clusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + taskArn: 'arn:aws:ecs:us-east-1:123456789012:task/abc123', + }, + }; + mockSend + .mockResolvedValueOnce({ Item: ecsTask }) + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({}); + + const result = await handler(makeEvent()); + expect(result.statusCode).toBe(200); + expect(mockEcsSend).toHaveBeenCalledTimes(1); + const cmd = mockEcsSend.mock.calls[0][0]; + expect(cmd.input.cluster).toBe('arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster'); + expect(cmd.input.task).toBe('arn:aws:ecs:us-east-1:123456789012:task/abc123'); + // AgentCore should NOT have been called + expect(mockAgentCoreSend).not.toHaveBeenCalled(); + }); + + test('skips ECS StopTask when compute_metadata.taskArn is missing', async () => { + mockSend.mockReset(); + const ecsTask = { + ...RUNNING_TASK, + compute_type: 'ecs', + compute_metadata: { + clusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + }, + }; + mockSend + .mockResolvedValueOnce({ Item: ecsTask }) + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({}); + + const result = await handler(makeEvent()); + expect(result.statusCode).toBe(200); + expect(mockEcsSend).not.toHaveBeenCalled(); + expect(mockAgentCoreSend).not.toHaveBeenCalled(); + }); + + test('falls back to AgentCore stop when compute_type is unrecognized but RUNTIME_ARN is available', async () => { + mockSend.mockReset(); + const unknownTask = { + ...RUNNING_TASK, + compute_type: 'unknown', + }; + delete (unknownTask as { agent_runtime_arn?: string }).agent_runtime_arn; + mockSend + .mockResolvedValueOnce({ Item: unknownTask }) + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({}); + + const result = await handler(makeEvent()); + expect(result.statusCode).toBe(200); + expect(mockEcsSend).not.toHaveBeenCalled(); + // Falls back to AgentCore because RUNTIME_ARN env var is set + expect(mockAgentCoreSend).toHaveBeenCalledTimes(1); + }); + + test('returns 200 when ECS StopTask fails', async () => { + mockSend.mockReset(); + mockEcsSend.mockRejectedValueOnce(new Error('ECS error')); + const ecsTask = { + ...RUNNING_TASK, + compute_type: 'ecs', + compute_metadata: { + clusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + taskArn: 'arn:aws:ecs:us-east-1:123456789012:task/abc123', + }, + }; + mockSend + .mockResolvedValueOnce({ Item: ecsTask }) + .mockResolvedValueOnce({}) + .mockResolvedValueOnce({}); + + const result = await handler(makeEvent()); + expect(result.statusCode).toBe(200); + expect(mockEcsSend).toHaveBeenCalled(); + }); }); diff --git a/cdk/test/handlers/orchestrate-task.test.ts b/cdk/test/handlers/orchestrate-task.test.ts index abaafa8..b21f0f8 100644 --- a/cdk/test/handlers/orchestrate-task.test.ts +++ b/cdk/test/handlers/orchestrate-task.test.ts @@ -76,7 +76,6 @@ import { loadBlueprintConfig, loadTask, pollTaskStatus, - startSession, transitionTask, } from '../../src/handlers/shared/orchestrator'; @@ -236,25 +235,6 @@ describe('hydrateAndTransition', () => { }); }); -describe('startSession', () => { - test('invokes agent runtime and transitions to RUNNING', async () => { - mockAgentCoreSend.mockResolvedValueOnce({}); // InvokeAgentRuntime - mockDdbSend.mockResolvedValue({}); // transitionTask + emitTaskEvent - - const sessionId = await startSession(baseTask as any, { repo_url: 'org/repo', task_id: 'TASK001' }); - // Session ID is a UUID v4 (36 chars), not a ULID - expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); - expect(mockAgentCoreSend).toHaveBeenCalledTimes(1); - const transitionCall = mockDdbSend.mock.calls.find( - (c: any[]) => c[0]._type === 'Update' && c[0].input.ExpressionAttributeValues?.[':toStatus'] === 'RUNNING', - ); - expect(transitionCall).toBeDefined(); - expect(transitionCall![0].input.ExpressionAttributeValues[':attr_agent_runtime_arn']).toBe( - 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test', - ); - }); -}); - describe('pollTaskStatus', () => { test('increments attempt count and reads status', async () => { mockDdbSend.mockResolvedValueOnce({ Item: { status: 'RUNNING' } }); @@ -547,21 +527,6 @@ describe('hydrateAndTransition with blueprint config', () => { }); }); -describe('startSession with blueprint config', () => { - test('uses blueprint runtime ARN override', async () => { - mockAgentCoreSend.mockResolvedValueOnce({}); - mockDdbSend.mockResolvedValue({}); - - await startSession(baseTask as any, { repo_url: 'org/repo', task_id: 'TASK001' }, { - compute_type: 'agentcore', - runtime_arn: 'arn:aws:bedrock-agentcore:us-east-1:123:runtime/custom', - }); - - const invokeCall = mockAgentCoreSend.mock.calls[0][0]; - expect(invokeCall.input.agentRuntimeArn).toBe('arn:aws:bedrock-agentcore:us-east-1:123:runtime/custom'); - }); -}); - describe('finalizeTask', () => { test('handles already-terminal task', async () => { mockDdbSend diff --git a/cdk/test/handlers/shared/compute-strategy.test.ts b/cdk/test/handlers/shared/compute-strategy.test.ts new file mode 100644 index 0000000..ef15bea --- /dev/null +++ b/cdk/test/handlers/shared/compute-strategy.test.ts @@ -0,0 +1,84 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ + BedrockAgentCoreClient: jest.fn(() => ({ send: jest.fn() })), + InvokeAgentRuntimeCommand: jest.fn(), + StopRuntimeSessionCommand: jest.fn(), +})); + +jest.mock('@aws-sdk/client-ecs', () => ({ + ECSClient: jest.fn(() => ({ send: jest.fn() })), + RunTaskCommand: jest.fn(), + DescribeTasksCommand: jest.fn(), + StopTaskCommand: jest.fn(), +})); + +jest.mock('@aws-sdk/client-ec2', () => ({ + EC2Client: jest.fn(() => ({ send: jest.fn() })), + DescribeInstancesCommand: jest.fn(), + CreateTagsCommand: jest.fn(), + DeleteTagsCommand: jest.fn(), +})); + +jest.mock('@aws-sdk/client-ssm', () => ({ + SSMClient: jest.fn(() => ({ send: jest.fn() })), + SendCommandCommand: jest.fn(), + GetCommandInvocationCommand: jest.fn(), + CancelCommandCommand: jest.fn(), +})); + +jest.mock('@aws-sdk/client-s3', () => ({ + S3Client: jest.fn(() => ({ send: jest.fn() })), + PutObjectCommand: jest.fn(), +})); + +import { resolveComputeStrategy } from '../../../src/handlers/shared/compute-strategy'; +import { AgentCoreComputeStrategy } from '../../../src/handlers/shared/strategies/agentcore-strategy'; +import { Ec2ComputeStrategy } from '../../../src/handlers/shared/strategies/ec2-strategy'; +import { EcsComputeStrategy } from '../../../src/handlers/shared/strategies/ecs-strategy'; + +describe('resolveComputeStrategy', () => { + test('returns AgentCoreComputeStrategy for compute_type agentcore', () => { + const strategy = resolveComputeStrategy({ + compute_type: 'agentcore', + runtime_arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test', + }); + expect(strategy).toBeInstanceOf(AgentCoreComputeStrategy); + expect(strategy.type).toBe('agentcore'); + }); + + test('returns EcsComputeStrategy for compute_type ecs', () => { + const strategy = resolveComputeStrategy({ + compute_type: 'ecs', + runtime_arn: 'arn:test', + }); + expect(strategy).toBeInstanceOf(EcsComputeStrategy); + expect(strategy.type).toBe('ecs'); + }); + + test('returns Ec2ComputeStrategy for compute_type ec2', () => { + const strategy = resolveComputeStrategy({ + compute_type: 'ec2', + runtime_arn: 'arn:test', + }); + expect(strategy).toBeInstanceOf(Ec2ComputeStrategy); + expect(strategy.type).toBe('ec2'); + }); +}); diff --git a/cdk/test/handlers/shared/preflight.test.ts b/cdk/test/handlers/shared/preflight.test.ts index 780e047..ee7b9f6 100644 --- a/cdk/test/handlers/shared/preflight.test.ts +++ b/cdk/test/handlers/shared/preflight.test.ts @@ -40,7 +40,7 @@ const mockFetch = jest.fn(); global.fetch = mockFetch as unknown as typeof fetch; const baseBlueprintConfig: BlueprintConfig = { - compute_type: 'AGENTCORE', + compute_type: 'agentcore', runtime_arn: 'arn:aws:bedrock:us-east-1:123456789012:agent-runtime/test', github_token_secret_arn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:github-token', }; diff --git a/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts new file mode 100644 index 0000000..46f3f7e --- /dev/null +++ b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts @@ -0,0 +1,205 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const mockSend = jest.fn(); +jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ + BedrockAgentCoreClient: jest.fn(() => ({ send: mockSend })), + InvokeAgentRuntimeCommand: jest.fn((input: unknown) => ({ _type: 'InvokeAgentRuntime', input })), + StopRuntimeSessionCommand: jest.fn((input: unknown) => ({ _type: 'StopRuntimeSession', input })), +})); + +import { BedrockAgentCoreClient } from '@aws-sdk/client-bedrock-agentcore'; +import { AgentCoreComputeStrategy } from '../../../../src/handlers/shared/strategies/agentcore-strategy'; + +const MockedClient = jest.mocked(BedrockAgentCoreClient); +const defaultRuntimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/default'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('AgentCoreComputeStrategy', () => { + test('type is agentcore', () => { + const strategy = new AgentCoreComputeStrategy(); + expect(strategy.type).toBe('agentcore'); + }); + + describe('startSession', () => { + test('invokes agent runtime and returns SessionHandle', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy(); + + const handle = await strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo', task_id: 'TASK001' }, + blueprintConfig: { compute_type: 'agentcore', runtime_arn: defaultRuntimeArn }, + }); + + expect(handle.sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + expect(handle.strategyType).toBe('agentcore'); + const acHandle = handle as Extract; + expect(acHandle.runtimeArn).toBe(defaultRuntimeArn); + expect(mockSend).toHaveBeenCalledTimes(1); + }); + + test('uses runtime_arn from blueprintConfig (single source of truth)', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy(); + const runtimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123:runtime/custom'; + + const handle = await strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo', task_id: 'TASK001' }, + blueprintConfig: { compute_type: 'agentcore', runtime_arn: runtimeArn }, + }); + + const acHandle = handle as Extract; + expect(acHandle.runtimeArn).toBe(runtimeArn); + const invokeCall = mockSend.mock.calls[0][0]; + expect(invokeCall.input.agentRuntimeArn).toBe(runtimeArn); + }); + + test('reuses shared BedrockAgentCoreClient across instances', async () => { + // The lazy singleton may already be initialized from prior tests. + // Record the current call count, then verify no additional constructor calls happen. + const callsBefore = MockedClient.mock.calls.length; + + mockSend.mockResolvedValue({}); + const strategy1 = new AgentCoreComputeStrategy(); + const strategy2 = new AgentCoreComputeStrategy(); + + await strategy1.startSession({ + taskId: 'T1', + payload: {}, + blueprintConfig: { compute_type: 'agentcore', runtime_arn: defaultRuntimeArn }, + }); + await strategy2.startSession({ + taskId: 'T2', + payload: {}, + blueprintConfig: { compute_type: 'agentcore', runtime_arn: defaultRuntimeArn }, + }); + + // Lazy singleton: at most one constructor call total across all strategy instances + const callsAfter = MockedClient.mock.calls.length; + expect(callsAfter - callsBefore).toBeLessThanOrEqual(1); + expect(mockSend).toHaveBeenCalledTimes(2); + }); + }); + + describe('pollSession', () => { + test('returns running status (AgentCore polling is done via DDB)', async () => { + const strategy = new AgentCoreComputeStrategy(); + const result = await strategy.pollSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }); + expect(result).toEqual({ status: 'running' }); + }); + }); + + describe('stopSession', () => { + test('sends StopRuntimeSessionCommand', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy(); + + await strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }); + + expect(mockSend).toHaveBeenCalledTimes(1); + const call = mockSend.mock.calls[0][0]; + expect(call.input.agentRuntimeArn).toBe(defaultRuntimeArn); + expect(call.input.runtimeSessionId).toBe('test-session'); + }); + + test('logs info for ResourceNotFoundException (session already gone)', async () => { + const err = new Error('Not found'); + err.name = 'ResourceNotFoundException'; + mockSend.mockRejectedValueOnce(err); + const strategy = new AgentCoreComputeStrategy(); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }), + ).resolves.toBeUndefined(); + }); + + test('logs error for ThrottlingException', async () => { + const err = new Error('Rate exceeded'); + err.name = 'ThrottlingException'; + mockSend.mockRejectedValueOnce(err); + const strategy = new AgentCoreComputeStrategy(); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }), + ).resolves.toBeUndefined(); + }); + + test('logs error for AccessDeniedException', async () => { + const err = new Error('Access denied'); + err.name = 'AccessDeniedException'; + mockSend.mockRejectedValueOnce(err); + const strategy = new AgentCoreComputeStrategy(); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }), + ).resolves.toBeUndefined(); + }); + + test('logs warn for unknown errors (best-effort)', async () => { + mockSend.mockRejectedValueOnce(new Error('Network error')); + const strategy = new AgentCoreComputeStrategy(); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + runtimeArn: defaultRuntimeArn, + }), + ).resolves.toBeUndefined(); + }); + + test('throws when handle is not agentcore type', async () => { + const strategy = new AgentCoreComputeStrategy(); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'ecs', + clusterArn: 'arn:test', + taskArn: 'arn:test', + }), + ).rejects.toThrow('stopSession called with non-agentcore handle'); + }); + }); +}); diff --git a/cdk/test/handlers/shared/strategies/ec2-strategy.test.ts b/cdk/test/handlers/shared/strategies/ec2-strategy.test.ts new file mode 100644 index 0000000..6873722 --- /dev/null +++ b/cdk/test/handlers/shared/strategies/ec2-strategy.test.ts @@ -0,0 +1,340 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const FLEET_TAG_KEY = 'bgagent:fleet'; +const FLEET_TAG_VALUE = 'test-fleet'; +const PAYLOAD_BUCKET = 'test-payload-bucket'; +const ECR_IMAGE = '123456789012.dkr.ecr.us-east-1.amazonaws.com/agent:latest'; +const INSTANCE_ID = 'i-0123456789abcdef0'; +const COMMAND_ID = 'cmd-0123456789abcdef0'; + +// Set env vars BEFORE import — Ec2ComputeStrategy reads them as module-level constants +process.env.EC2_FLEET_TAG_KEY = FLEET_TAG_KEY; +process.env.EC2_FLEET_TAG_VALUE = FLEET_TAG_VALUE; +process.env.EC2_PAYLOAD_BUCKET = PAYLOAD_BUCKET; +process.env.ECR_IMAGE_URI = ECR_IMAGE; + +const mockEc2Send = jest.fn(); +jest.mock('@aws-sdk/client-ec2', () => ({ + EC2Client: jest.fn(() => ({ send: mockEc2Send })), + DescribeInstancesCommand: jest.fn((input: unknown) => ({ _type: 'DescribeInstances', input })), + CreateTagsCommand: jest.fn((input: unknown) => ({ _type: 'CreateTags', input })), + DeleteTagsCommand: jest.fn((input: unknown) => ({ _type: 'DeleteTags', input })), +})); + +const mockSsmSend = jest.fn(); +jest.mock('@aws-sdk/client-ssm', () => ({ + SSMClient: jest.fn(() => ({ send: mockSsmSend })), + SendCommandCommand: jest.fn((input: unknown) => ({ _type: 'SendCommand', input })), + GetCommandInvocationCommand: jest.fn((input: unknown) => ({ _type: 'GetCommandInvocation', input })), + CancelCommandCommand: jest.fn((input: unknown) => ({ _type: 'CancelCommand', input })), +})); + +const mockS3Send = jest.fn(); +jest.mock('@aws-sdk/client-s3', () => ({ + S3Client: jest.fn(() => ({ send: mockS3Send })), + PutObjectCommand: jest.fn((input: unknown) => ({ _type: 'PutObject', input })), +})); + +import { Ec2ComputeStrategy } from '../../../../src/handlers/shared/strategies/ec2-strategy'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('Ec2ComputeStrategy', () => { + test('type is ec2', () => { + const strategy = new Ec2ComputeStrategy(); + expect(strategy.type).toBe('ec2'); + }); + + describe('startSession', () => { + test('finds idle instance, tags as busy, uploads to S3, sends SSM command, returns handle', async () => { + // S3 upload + mockS3Send.mockResolvedValueOnce({}); + // DescribeInstances — return one idle instance + mockEc2Send.mockResolvedValueOnce({ + Reservations: [{ Instances: [{ InstanceId: INSTANCE_ID }] }], + }); + // CreateTags (mark busy) + mockEc2Send.mockResolvedValueOnce({}); + // SSM SendCommand + mockSsmSend.mockResolvedValueOnce({ + Command: { CommandId: COMMAND_ID }, + }); + + const strategy = new Ec2ComputeStrategy(); + const handle = await strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo', prompt: 'Fix the bug', issue_number: 42, max_turns: 50 }, + blueprintConfig: { compute_type: 'ec2', runtime_arn: '' }, + }); + + expect(handle.sessionId).toBe(COMMAND_ID); + expect(handle.strategyType).toBe('ec2'); + const ec2Handle = handle as Extract; + expect(ec2Handle.instanceId).toBe(INSTANCE_ID); + expect(ec2Handle.commandId).toBe(COMMAND_ID); + + // Verify S3 upload + expect(mockS3Send).toHaveBeenCalledTimes(1); + const s3Call = mockS3Send.mock.calls[0][0]; + expect(s3Call.input.Bucket).toBe(PAYLOAD_BUCKET); + expect(s3Call.input.Key).toBe('tasks/TASK001/payload.json'); + + // Verify DescribeInstances filter + expect(mockEc2Send).toHaveBeenCalledTimes(2); + const describeCall = mockEc2Send.mock.calls[0][0]; + expect(describeCall.input.Filters).toEqual(expect.arrayContaining([ + expect.objectContaining({ Name: `tag:${FLEET_TAG_KEY}`, Values: [FLEET_TAG_VALUE] }), + expect.objectContaining({ Name: 'instance-state-name', Values: ['running'] }), + expect.objectContaining({ Name: 'tag:bgagent:status', Values: ['idle'] }), + ])); + + // Verify CreateTags (busy) + const tagCall = mockEc2Send.mock.calls[1][0]; + expect(tagCall.input.Resources).toEqual([INSTANCE_ID]); + expect(tagCall.input.Tags).toEqual(expect.arrayContaining([ + { Key: 'bgagent:status', Value: 'busy' }, + { Key: 'bgagent:task-id', Value: 'TASK001' }, + ])); + + // Verify SSM SendCommand + expect(mockSsmSend).toHaveBeenCalledTimes(1); + const ssmCall = mockSsmSend.mock.calls[0][0]; + expect(ssmCall.input.DocumentName).toBe('AWS-RunShellScript'); + expect(ssmCall.input.InstanceIds).toEqual([INSTANCE_ID]); + expect(ssmCall.input.TimeoutSeconds).toBe(32400); + }); + + test('throws when no idle instances available', async () => { + // S3 upload + mockS3Send.mockResolvedValueOnce({}); + // DescribeInstances — return empty + mockEc2Send.mockResolvedValueOnce({ Reservations: [] }); + + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo' }, + blueprintConfig: { compute_type: 'ec2', runtime_arn: '' }, + }), + ).rejects.toThrow('No idle EC2 instances available in fleet'); + }); + + test('throws when SSM SendCommand fails', async () => { + // S3 upload + mockS3Send.mockResolvedValueOnce({}); + // DescribeInstances + mockEc2Send.mockResolvedValueOnce({ + Reservations: [{ Instances: [{ InstanceId: INSTANCE_ID }] }], + }); + // CreateTags + mockEc2Send.mockResolvedValueOnce({}); + // SSM SendCommand — return no CommandId + mockSsmSend.mockResolvedValueOnce({ Command: {} }); + + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo' }, + blueprintConfig: { compute_type: 'ec2', runtime_arn: '' }, + }), + ).rejects.toThrow('SSM SendCommand returned no CommandId'); + }); + }); + + describe('pollSession', () => { + const makeHandle = () => ({ + sessionId: COMMAND_ID, + strategyType: 'ec2' as const, + instanceId: INSTANCE_ID, + commandId: COMMAND_ID, + }); + + test('returns running for InProgress status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'InProgress' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns running for Pending status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Pending' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns running for Delayed status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Delayed' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns completed for Success status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Success' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'completed' }); + }); + + test('returns failed for Failed status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Failed', StatusDetails: 'Script exited with code 1' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'failed', error: 'Script exited with code 1' }); + }); + + test('returns failed for Cancelled status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Cancelled', StatusDetails: 'Cancelled by user' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'failed', error: 'Cancelled by user' }); + }); + + test('returns failed for TimedOut status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'TimedOut', StatusDetails: 'Command timed out' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'failed', error: 'Command timed out' }); + }); + + test('returns failed for Cancelling status', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'Cancelling', StatusDetails: 'Command is being cancelled' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'failed', error: 'Command is being cancelled' }); + }); + + test('returns running for unknown status (default case)', async () => { + mockSsmSend.mockResolvedValueOnce({ Status: 'SomeUnknownStatus' }); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns failed when InvocationDoesNotExist', async () => { + const err = new Error('Invocation does not exist'); + err.name = 'InvocationDoesNotExist'; + mockSsmSend.mockRejectedValueOnce(err); + + const strategy = new Ec2ComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'failed', error: 'SSM command invocation not found' }); + }); + + test('throws when handle is not ec2 type', async () => { + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.pollSession({ + sessionId: 'test', + strategyType: 'agentcore', + runtimeArn: 'arn:test', + }), + ).rejects.toThrow('pollSession called with non-ec2 handle'); + }); + }); + + describe('stopSession', () => { + test('cancels SSM command and tags instance idle', async () => { + // CancelCommand + mockSsmSend.mockResolvedValueOnce({}); + // CreateTags (idle) + mockEc2Send.mockResolvedValueOnce({}); + // DeleteTags (task-id) + mockEc2Send.mockResolvedValueOnce({}); + + const strategy = new Ec2ComputeStrategy(); + await strategy.stopSession({ + sessionId: COMMAND_ID, + strategyType: 'ec2', + instanceId: INSTANCE_ID, + commandId: COMMAND_ID, + }); + + expect(mockSsmSend).toHaveBeenCalledTimes(1); + const ssmCall = mockSsmSend.mock.calls[0][0]; + expect(ssmCall.input.CommandId).toBe(COMMAND_ID); + expect(ssmCall.input.InstanceIds).toEqual([INSTANCE_ID]); + + // Verify instance tagged back to idle + expect(mockEc2Send).toHaveBeenCalledTimes(2); + }); + + test('handles already-cancelled command gracefully', async () => { + const err = new Error('Invalid command'); + err.name = 'InvalidCommandId'; + mockSsmSend.mockRejectedValueOnce(err); + // Cleanup tags still attempted + mockEc2Send.mockResolvedValueOnce({}); + mockEc2Send.mockResolvedValueOnce({}); + + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: COMMAND_ID, + strategyType: 'ec2', + instanceId: INSTANCE_ID, + commandId: COMMAND_ID, + }), + ).resolves.toBeUndefined(); + }); + + test('throws when handle is not ec2 type', async () => { + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: 'test', + strategyType: 'agentcore', + runtimeArn: 'arn:test', + }), + ).rejects.toThrow('stopSession called with non-ec2 handle'); + }); + + test('swallows tag cleanup errors gracefully', async () => { + // CancelCommand succeeds + mockSsmSend.mockResolvedValueOnce({}); + // CreateTags fails (instance terminated) + mockEc2Send.mockRejectedValueOnce(new Error('Instance terminated')); + + const strategy = new Ec2ComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: COMMAND_ID, + strategyType: 'ec2', + instanceId: INSTANCE_ID, + commandId: COMMAND_ID, + }), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/cdk/test/handlers/shared/strategies/ecs-strategy.test.ts b/cdk/test/handlers/shared/strategies/ecs-strategy.test.ts new file mode 100644 index 0000000..0e365a1 --- /dev/null +++ b/cdk/test/handlers/shared/strategies/ecs-strategy.test.ts @@ -0,0 +1,335 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const CLUSTER_ARN = 'arn:aws:ecs:us-east-1:123456789012:cluster/test-cluster'; +const TASK_DEF_ARN = 'arn:aws:ecs:us-east-1:123456789012:task-definition/agent:1'; +const TASK_ARN = 'arn:aws:ecs:us-east-1:123456789012:task/test-cluster/abc123'; + +// Set env vars BEFORE import — EcsComputeStrategy reads them as module-level constants +process.env.ECS_CLUSTER_ARN = CLUSTER_ARN; +process.env.ECS_TASK_DEFINITION_ARN = TASK_DEF_ARN; +process.env.ECS_SUBNETS = 'subnet-aaa,subnet-bbb'; +process.env.ECS_SECURITY_GROUP = 'sg-12345'; +process.env.ECS_CONTAINER_NAME = 'AgentContainer'; + +const mockSend = jest.fn(); +jest.mock('@aws-sdk/client-ecs', () => ({ + ECSClient: jest.fn(() => ({ send: mockSend })), + RunTaskCommand: jest.fn((input: unknown) => ({ _type: 'RunTask', input })), + DescribeTasksCommand: jest.fn((input: unknown) => ({ _type: 'DescribeTasks', input })), + StopTaskCommand: jest.fn((input: unknown) => ({ _type: 'StopTask', input })), +})); + +import { EcsComputeStrategy } from '../../../../src/handlers/shared/strategies/ecs-strategy'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('EcsComputeStrategy', () => { + test('type is ecs', () => { + const strategy = new EcsComputeStrategy(); + expect(strategy.type).toBe('ecs'); + }); + + describe('startSession', () => { + test('sends RunTaskCommand with correct params and returns SessionHandle', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ taskArn: TASK_ARN }], + }); + + const strategy = new EcsComputeStrategy(); + const handle = await strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo', prompt: 'Fix the bug', issue_number: 42, max_turns: 50 }, + blueprintConfig: { compute_type: 'ecs', runtime_arn: '' }, + }); + + expect(handle.sessionId).toBe(TASK_ARN); + expect(handle.strategyType).toBe('ecs'); + const ecsHandle = handle as Extract; + expect(ecsHandle.clusterArn).toBe(CLUSTER_ARN); + expect(ecsHandle.taskArn).toBe(TASK_ARN); + expect(mockSend).toHaveBeenCalledTimes(1); + + const call = mockSend.mock.calls[0][0]; + expect(call.input.cluster).toBe(CLUSTER_ARN); + expect(call.input.taskDefinition).toBe(TASK_DEF_ARN); + expect(call.input.launchType).toBe('FARGATE'); + expect(call.input.networkConfiguration.awsvpcConfiguration.subnets).toEqual(['subnet-aaa', 'subnet-bbb']); + expect(call.input.networkConfiguration.awsvpcConfiguration.securityGroups).toEqual(['sg-12345']); + expect(call.input.networkConfiguration.awsvpcConfiguration.assignPublicIp).toBe('DISABLED'); + + const override = call.input.overrides.containerOverrides[0]; + const envVars = override.environment; + expect(envVars).toEqual(expect.arrayContaining([ + { name: 'TASK_ID', value: 'TASK001' }, + { name: 'REPO_URL', value: 'org/repo' }, + { name: 'TASK_DESCRIPTION', value: 'Fix the bug' }, + { name: 'ISSUE_NUMBER', value: '42' }, + { name: 'MAX_TURNS', value: '50' }, + { name: 'CLAUDE_CODE_USE_BEDROCK', value: '1' }, + ])); + + // AGENT_PAYLOAD contains the full orchestrator payload for direct run_task() invocation + const agentPayload = envVars.find((e: { name: string }) => e.name === 'AGENT_PAYLOAD'); + expect(agentPayload).toBeDefined(); + const parsed = JSON.parse(agentPayload.value); + expect(parsed.repo_url).toBe('org/repo'); + expect(parsed.prompt).toBe('Fix the bug'); + + // Container command override — runs Python directly instead of uvicorn + expect(override.command).toBeDefined(); + expect(override.command[0]).toBe('python'); + }); + + test('throws when RunTask returns no task', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [], + failures: [{ arn: 'arn:test', reason: 'RESOURCE:ENI' }], + }); + + const strategy = new EcsComputeStrategy(); + await expect( + strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo' }, + blueprintConfig: { compute_type: 'ecs', runtime_arn: '' }, + }), + ).rejects.toThrow('ECS RunTask returned no task: arn:test: RESOURCE:ENI'); + }); + + test('includes model_id and system_prompt_overrides from blueprintConfig', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ taskArn: TASK_ARN }], + }); + + const strategy = new EcsComputeStrategy(); + await strategy.startSession({ + taskId: 'TASK001', + payload: { repo_url: 'org/repo' }, + blueprintConfig: { + compute_type: 'ecs', + runtime_arn: '', + model_id: 'anthropic.claude-sonnet-4-6', + system_prompt_overrides: 'Be concise', + }, + }); + + const call = mockSend.mock.calls[0][0]; + const envVars = call.input.overrides.containerOverrides[0].environment; + expect(envVars).toEqual(expect.arrayContaining([ + { name: 'ANTHROPIC_MODEL', value: 'anthropic.claude-sonnet-4-6' }, + { name: 'SYSTEM_PROMPT_OVERRIDES', value: 'Be concise' }, + ])); + }); + }); + + describe('pollSession', () => { + const makeHandle = () => ({ + sessionId: TASK_ARN, + strategyType: 'ecs' as const, + clusterArn: CLUSTER_ARN, + taskArn: TASK_ARN, + }); + + test('returns running for RUNNING status', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ lastStatus: 'RUNNING' }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns running for PENDING status', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ lastStatus: 'PENDING' }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'running' }); + }); + + test('returns completed for STOPPED with exit code 0', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ + lastStatus: 'STOPPED', + containers: [{ exitCode: 0 }], + }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ status: 'completed' }); + }); + + test('returns failed for STOPPED with undefined exit code (container never started)', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ + lastStatus: 'STOPPED', + stoppedReason: 'CannotPullContainerError', + containers: [{}], + }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ + status: 'failed', + error: 'Task stopped: CannotPullContainerError', + }); + }); + + test('returns failed for STOPPED with no containers', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ + lastStatus: 'STOPPED', + stoppedReason: 'EssentialContainerExited', + containers: [], + }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ + status: 'failed', + error: 'Task stopped: EssentialContainerExited', + }); + }); + + test('returns failed for STOPPED with non-zero exit code', async () => { + mockSend.mockResolvedValueOnce({ + tasks: [{ + lastStatus: 'STOPPED', + stoppedReason: 'OutOfMemoryError', + containers: [{ exitCode: 137 }], + }], + }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ + status: 'failed', + error: 'Exit code 137: OutOfMemoryError', + }); + }); + + test('returns failed when task not found', async () => { + mockSend.mockResolvedValueOnce({ tasks: [] }); + + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession(makeHandle()); + expect(result).toEqual({ + status: 'failed', + error: `ECS task ${TASK_ARN} not found`, + }); + }); + + test('throws when handle is not ecs type', async () => { + const strategy = new EcsComputeStrategy(); + await expect( + strategy.pollSession({ + sessionId: 'test', + strategyType: 'agentcore', + runtimeArn: 'arn:test', + }), + ).rejects.toThrow('pollSession called with non-ecs handle'); + }); + }); + + describe('stopSession', () => { + test('sends StopTaskCommand', async () => { + mockSend.mockResolvedValueOnce({}); + + const strategy = new EcsComputeStrategy(); + await strategy.stopSession({ + sessionId: TASK_ARN, + strategyType: 'ecs', + clusterArn: CLUSTER_ARN, + taskArn: TASK_ARN, + }); + + expect(mockSend).toHaveBeenCalledTimes(1); + const call = mockSend.mock.calls[0][0]; + expect(call.input.cluster).toBe(CLUSTER_ARN); + expect(call.input.task).toBe(TASK_ARN); + expect(call.input.reason).toBe('Stopped by orchestrator'); + }); + + test('handles InvalidParameterException gracefully', async () => { + const err = new Error('Invalid'); + err.name = 'InvalidParameterException'; + mockSend.mockRejectedValueOnce(err); + + const strategy = new EcsComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: TASK_ARN, + strategyType: 'ecs', + clusterArn: CLUSTER_ARN, + taskArn: TASK_ARN, + }), + ).resolves.toBeUndefined(); + }); + + test('handles ResourceNotFoundException gracefully', async () => { + const err = new Error('Not found'); + err.name = 'ResourceNotFoundException'; + mockSend.mockRejectedValueOnce(err); + + const strategy = new EcsComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: TASK_ARN, + strategyType: 'ecs', + clusterArn: CLUSTER_ARN, + taskArn: TASK_ARN, + }), + ).resolves.toBeUndefined(); + }); + + test('throws when handle is not ecs type', async () => { + const strategy = new EcsComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: 'test', + strategyType: 'agentcore', + runtimeArn: 'arn:test', + }), + ).rejects.toThrow('stopSession called with non-ecs handle'); + }); + + test('logs error for unknown errors (best-effort)', async () => { + mockSend.mockRejectedValueOnce(new Error('Network error')); + + const strategy = new EcsComputeStrategy(); + await expect( + strategy.stopSession({ + sessionId: TASK_ARN, + strategyType: 'ecs', + clusterArn: CLUSTER_ARN, + taskArn: TASK_ARN, + }), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/cdk/test/handlers/start-session-composition.test.ts b/cdk/test/handlers/start-session-composition.test.ts new file mode 100644 index 0000000..e9592d6 --- /dev/null +++ b/cdk/test/handlers/start-session-composition.test.ts @@ -0,0 +1,158 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Integration-style tests for the start-session step composition: + * resolveComputeStrategy → strategy.startSession → transitionTask → emitTaskEvent + * These verify that the orchestrate-task handler's step 4 logic correctly + * wires the strategy, state transitions, and event emission together. + */ + +const mockDdbSend = jest.fn(); +jest.mock('@aws-sdk/client-dynamodb', () => ({ DynamoDBClient: jest.fn(() => ({})) })); +jest.mock('@aws-sdk/lib-dynamodb', () => ({ + DynamoDBDocumentClient: { from: jest.fn(() => ({ send: mockDdbSend })) }, + GetCommand: jest.fn((input: unknown) => ({ _type: 'Get', input })), + PutCommand: jest.fn((input: unknown) => ({ _type: 'Put', input })), + UpdateCommand: jest.fn((input: unknown) => ({ _type: 'Update', input })), +})); + +const mockAgentCoreSend = jest.fn(); +jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ + BedrockAgentCoreClient: jest.fn(() => ({ send: mockAgentCoreSend })), + InvokeAgentRuntimeCommand: jest.fn((input: unknown) => ({ _type: 'InvokeAgentRuntime', input })), + StopRuntimeSessionCommand: jest.fn((input: unknown) => ({ _type: 'StopRuntimeSession', input })), +})); + +jest.mock('../../src/handlers/shared/repo-config', () => ({ + loadRepoConfig: jest.fn(), + checkRepoOnboarded: jest.fn(), +})); + +jest.mock('../../src/handlers/shared/memory', () => ({ + writeMinimalEpisode: jest.fn(), +})); + +jest.mock('../../src/handlers/shared/prompt-version', () => ({ + computePromptVersion: jest.fn().mockReturnValue('abc123'), +})); + +jest.mock('../../src/handlers/shared/context-hydration', () => ({ + hydrateContext: jest.fn(), +})); + +let ulidCounter = 0; +jest.mock('ulid', () => ({ ulid: jest.fn(() => `ULID${ulidCounter++}`) })); + +process.env.TASK_TABLE_NAME = 'Tasks'; +process.env.TASK_EVENTS_TABLE_NAME = 'TaskEvents'; +process.env.USER_CONCURRENCY_TABLE_NAME = 'UserConcurrency'; +process.env.RUNTIME_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test'; +process.env.MAX_CONCURRENT_TASKS_PER_USER = '3'; +process.env.TASK_RETENTION_DAYS = '90'; + +import { TaskStatus } from '../../src/constructs/task-status'; +import { resolveComputeStrategy } from '../../src/handlers/shared/compute-strategy'; +import { transitionTask, emitTaskEvent, failTask } from '../../src/handlers/shared/orchestrator'; +import type { BlueprintConfig } from '../../src/handlers/shared/repo-config'; + +beforeEach(() => { + jest.clearAllMocks(); + ulidCounter = 0; +}); + +describe('start-session step composition', () => { + const taskId = 'TASK001'; + const userId = 'user-123'; + const blueprintConfig: BlueprintConfig = { + compute_type: 'agentcore', + runtime_arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/test', + }; + const payload = { repo_url: 'org/repo', task_id: taskId }; + + test('happy path: strategy.startSession → transitionTask → emitTaskEvent', async () => { + mockAgentCoreSend.mockResolvedValueOnce({}); + mockDdbSend.mockResolvedValue({}); // transitionTask + emitTaskEvent + + const strategy = resolveComputeStrategy(blueprintConfig); + const handle = await strategy.startSession({ taskId, payload, blueprintConfig }); + + await transitionTask(taskId, TaskStatus.HYDRATING, TaskStatus.RUNNING, { + session_id: handle.sessionId, + started_at: expect.any(String), + }); + await emitTaskEvent(taskId, 'session_started', { + session_id: handle.sessionId, + strategy_type: handle.strategyType, + }); + + // Verify AgentCore was invoked + expect(mockAgentCoreSend).toHaveBeenCalledTimes(1); + // Verify DDB was called for transition + event + expect(mockDdbSend).toHaveBeenCalledTimes(2); + // Verify handle shape + expect(handle.strategyType).toBe('agentcore'); + expect(handle.sessionId).toBeDefined(); + }); + + test('error path: strategy.startSession fails → failTask is called', async () => { + mockAgentCoreSend.mockRejectedValueOnce(new Error('InvokeAgent failed')); + mockDdbSend.mockResolvedValue({}); // failTask transitions + + const strategy = resolveComputeStrategy(blueprintConfig); + + try { + await strategy.startSession({ taskId, payload, blueprintConfig }); + fail('Expected startSession to throw'); + } catch (err) { + await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, userId, true); + } + + // failTask should have been called — transitions to FAILED + emits event + decrements concurrency + // transitionTask (1) + emitTaskEvent (1) + decrementConcurrency (1) = 3 DDB calls + expect(mockDdbSend).toHaveBeenCalledTimes(3); + }); + + test('partial failure: strategy succeeds but transitionTask throws', async () => { + mockAgentCoreSend.mockResolvedValueOnce({}); + const condErr = new Error('Conditional check failed'); + condErr.name = 'ConditionalCheckFailedException'; + mockDdbSend + .mockRejectedValueOnce(condErr) // transitionTask fails + .mockResolvedValue({}); // failTask calls + + const strategy = resolveComputeStrategy(blueprintConfig); + const handle = await strategy.startSession({ taskId, payload, blueprintConfig }); + + try { + await transitionTask(taskId, TaskStatus.HYDRATING, TaskStatus.RUNNING, { + session_id: handle.sessionId, + started_at: new Date().toISOString(), + }); + fail('Expected transitionTask to throw'); + } catch (err) { + await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, userId, true); + } + + // AgentCore was invoked + expect(mockAgentCoreSend).toHaveBeenCalledTimes(1); + // transitionTask failed (1) + failTask: transitionTask (1) + emitTaskEvent (1) + decrement (1) = 4 + expect(mockDdbSend).toHaveBeenCalledTimes(4); + }); +}); diff --git a/docs/design/ARCHITECTURE.md b/docs/design/ARCHITECTURE.md index 662356a..baa953a 100644 --- a/docs/design/ARCHITECTURE.md +++ b/docs/design/ARCHITECTURE.md @@ -90,6 +90,20 @@ The steps below are the blueprint in action: deterministic orchestration (1–2, 3. **Agentic:** The agent runs in the isolated environment: clone repo, create branch, edit code, commit often, run tests and lint, create PR. Commits are attributed via git trailers (`Task-Id`, `Prompt-Version`). At task end, the agent writes memory (task episode + repo learnings) to AgentCore Memory. The orchestrator does not execute this logic; it only waits for the session to finish. 4. **Deterministic:** The orchestrator infers the result (e.g. by querying GitHub for a PR on the agent's branch), updates task status, and finalizes (result inference, cleanup). If the agent did not write memory (crash, timeout), the orchestrator writes a fallback episode. A validation step may run here (e.g. configurable post-agent checks); see repo onboarding for customizing these steps. +### Why the orchestrator and agent are separate loops + +The orchestrator (deterministic) and the agent workload (non-deterministic) could in theory run as a single process, but they are deliberately separated. This separation is the architectural foundation for several guarantees: + +**Reliability boundary.** The agent is the component most likely to fail — LLM hallucination, OOM, session crash, idle timeout. The orchestrator wraps the agent with durable execution (checkpoint/resume via Lambda Durable Functions) so that when the agent dies mid-task, the platform still drives the task to a terminal state: it detects the failure via heartbeat/poll, transitions the task to FAILED or TIMED_OUT, releases concurrency counters, writes a fallback memory episode, and emits cleanup events. Without this boundary, a crashed agent would leave orphaned state — stuck counters, no terminal status, no user notification. + +**Cost separation.** Orchestrator steps are Lambda invocations costing fractions of a cent. Agent steps burn compute-hours and LLM inference tokens (the dominant cost at $0.20–0.60 per task). Keeping admission control, context hydration, result inference, and finalization out of the compute session avoids paying compute and token costs for bookkeeping work that requires no LLM reasoning. + +**Trust boundary.** The agent runs inside a sandboxed MicroVM (AgentCore Runtime) with a blast radius limited to one branch in one repository. The orchestrator runs in the trusted platform layer (Lambda + DynamoDB) and enforces invariants the agent cannot bypass: concurrency limits, cancellation, timeout enforcement, and conditional state transitions (`ConditionExpression` guards on DynamoDB writes). The agent's own state writes are guarded to prevent it from overwriting orchestrator-managed status (e.g. an agent writing COMPLETED over an orchestrator-set CANCELLED). + +**Testability.** Deterministic steps can be unit-tested without LLM calls, compute sessions, or GitHub API access. The orchestrator's admission control, context hydration, result inference, and state transitions are covered by fast, isolated Jest tests (`cdk/test/handlers/shared/`). The agent workload requires integration testing with a live model and compute environment. Keeping them separate means platform logic can be validated cheaply and quickly, independent of model behavior. + +**Independent evolution.** The orchestrator and agent communicate through a narrow contract: the orchestrator passes a hydrated prompt and environment variables; the agent pushes commits, creates a PR, and exits. Either side can change independently as long as the contract holds — the orchestrator can add new pre/post steps, switch durable execution engines, or change polling strategies without touching the agent code, and the agent can change its tool set, prompting strategy, or coding workflow without affecting the orchestrator. + For the API contract — endpoints, request/response schemas, error codes, authentication, and pagination — see [API_CONTRACT.md](./API_CONTRACT.md). ## Onboarding pipeline diff --git a/docs/src/content/docs/design/Architecture.md b/docs/src/content/docs/design/Architecture.md index e069011..97ea314 100644 --- a/docs/src/content/docs/design/Architecture.md +++ b/docs/src/content/docs/design/Architecture.md @@ -94,6 +94,20 @@ The steps below are the blueprint in action: deterministic orchestration (1–2, 3. **Agentic:** The agent runs in the isolated environment: clone repo, create branch, edit code, commit often, run tests and lint, create PR. Commits are attributed via git trailers (`Task-Id`, `Prompt-Version`). At task end, the agent writes memory (task episode + repo learnings) to AgentCore Memory. The orchestrator does not execute this logic; it only waits for the session to finish. 4. **Deterministic:** The orchestrator infers the result (e.g. by querying GitHub for a PR on the agent's branch), updates task status, and finalizes (result inference, cleanup). If the agent did not write memory (crash, timeout), the orchestrator writes a fallback episode. A validation step may run here (e.g. configurable post-agent checks); see repo onboarding for customizing these steps. +### Why the orchestrator and agent are separate loops + +The orchestrator (deterministic) and the agent workload (non-deterministic) could in theory run as a single process, but they are deliberately separated. This separation is the architectural foundation for several guarantees: + +**Reliability boundary.** The agent is the component most likely to fail — LLM hallucination, OOM, session crash, idle timeout. The orchestrator wraps the agent with durable execution (checkpoint/resume via Lambda Durable Functions) so that when the agent dies mid-task, the platform still drives the task to a terminal state: it detects the failure via heartbeat/poll, transitions the task to FAILED or TIMED_OUT, releases concurrency counters, writes a fallback memory episode, and emits cleanup events. Without this boundary, a crashed agent would leave orphaned state — stuck counters, no terminal status, no user notification. + +**Cost separation.** Orchestrator steps are Lambda invocations costing fractions of a cent. Agent steps burn compute-hours and LLM inference tokens (the dominant cost at $0.20–0.60 per task). Keeping admission control, context hydration, result inference, and finalization out of the compute session avoids paying compute and token costs for bookkeeping work that requires no LLM reasoning. + +**Trust boundary.** The agent runs inside a sandboxed MicroVM (AgentCore Runtime) with a blast radius limited to one branch in one repository. The orchestrator runs in the trusted platform layer (Lambda + DynamoDB) and enforces invariants the agent cannot bypass: concurrency limits, cancellation, timeout enforcement, and conditional state transitions (`ConditionExpression` guards on DynamoDB writes). The agent's own state writes are guarded to prevent it from overwriting orchestrator-managed status (e.g. an agent writing COMPLETED over an orchestrator-set CANCELLED). + +**Testability.** Deterministic steps can be unit-tested without LLM calls, compute sessions, or GitHub API access. The orchestrator's admission control, context hydration, result inference, and state transitions are covered by fast, isolated Jest tests (`cdk/test/handlers/shared/`). The agent workload requires integration testing with a live model and compute environment. Keeping them separate means platform logic can be validated cheaply and quickly, independent of model behavior. + +**Independent evolution.** The orchestrator and agent communicate through a narrow contract: the orchestrator passes a hydrated prompt and environment variables; the agent pushes commits, creates a PR, and exits. Either side can change independently as long as the contract holds — the orchestrator can add new pre/post steps, switch durable execution engines, or change polling strategies without touching the agent code, and the agent can change its tool set, prompting strategy, or coding workflow without affecting the orchestrator. + For the API contract — endpoints, request/response schemas, error codes, authentication, and pagination — see [API_CONTRACT.md](/design/api-contract). ## Onboarding pipeline diff --git a/yarn.lock b/yarn.lock index ecdc6cd..9ede11c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,27 @@ "@aws-sdk/types" "^3.222.0" tslib "^2.6.2" +"@aws-crypto/crc32c@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" + integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== + dependencies: + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/sha1-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz#b0ee2d2821d3861f017e965ef3b4cb38e3b6a0f4" + integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== + dependencies: + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + "@aws-crypto/sha256-browser@5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" @@ -250,7 +271,7 @@ dependencies: tslib "^2.6.2" -"@aws-crypto/util@^5.2.0": +"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== @@ -454,6 +475,99 @@ "@smithy/util-waiter" "^4.2.14" tslib "^2.6.2" +"@aws-sdk/client-ec2@^3.1021.0": + version "3.1030.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ec2/-/client-ec2-3.1030.0.tgz#1207c91571ec51d07e07f1c1454b6706d9fbc7e2" + integrity sha512-jvi++FA3GWdl0ryaT4AQo1yQaSiTzQzGZEKX+aNbHswBFTu/3sBOrIBUIsn+VrZmrbleUQ53h41ADaKrJ93NUw== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-node" "^3.972.30" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-sdk-ec2" "^3.972.19" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-utf8" "^4.2.2" + "@smithy/util-waiter" "^4.2.15" + tslib "^2.6.2" + +"@aws-sdk/client-ecs@^3.1021.0": + version "3.1027.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ecs/-/client-ecs-3.1027.0.tgz#fdc05b3c8a8d9457776791cb3ac4acb57da298a2" + integrity sha512-HS6Ca0kX8agG5D/+wHsTNkbgawTyXPgxwDA3KwuRBXU5e/BzK+gHVBOya+IEmUPr25IvEfY8hQm4yd0xyXBUPw== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-node" "^3.972.30" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-utf8" "^4.2.2" + "@smithy/util-waiter" "^4.2.15" + tslib "^2.6.2" + "@aws-sdk/client-lambda@^3.1021.0", "@aws-sdk/client-lambda@^3.943.0": version "3.1021.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-lambda/-/client-lambda-3.1021.0.tgz#6f757ba466b686fb04128e931522fa5a33cf00d8" @@ -504,6 +618,67 @@ "@smithy/util-waiter" "^4.2.14" tslib "^2.6.2" +"@aws-sdk/client-s3@^3.1021.0": + version "3.1030.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1030.0.tgz#f5c593deb0e32fbd0a174d00feae9c69c0e7cccf" + integrity sha512-sgGb4ub0JXnHaXnok5td7A1KGwENFPwOrwgzvpkeWq9w16Sl7x2KhYtVl+Fdd/7LAvaEtm3HqrYtNmm2d0OXmQ== + dependencies: + "@aws-crypto/sha1-browser" "5.2.0" + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-node" "^3.972.30" + "@aws-sdk/middleware-bucket-endpoint" "^3.972.9" + "@aws-sdk/middleware-expect-continue" "^3.972.9" + "@aws-sdk/middleware-flexible-checksums" "^3.974.7" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-location-constraint" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-sdk-s3" "^3.972.28" + "@aws-sdk/middleware-ssec" "^3.972.9" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/signature-v4-multi-region" "^3.996.16" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/eventstream-serde-browser" "^4.2.13" + "@smithy/eventstream-serde-config-resolver" "^4.3.13" + "@smithy/eventstream-serde-node" "^4.2.13" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-blob-browser" "^4.2.14" + "@smithy/hash-node" "^4.2.13" + "@smithy/hash-stream-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/md5-js" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + "@smithy/util-waiter" "^4.2.15" + tslib "^2.6.2" + "@aws-sdk/client-secrets-manager@^3.1021.0": version "3.1021.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1021.0.tgz#57c6348c63146642132ffa7e885a2abba08c6ff4" @@ -549,6 +724,52 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@aws-sdk/client-ssm@^3.1021.0": + version "3.1030.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ssm/-/client-ssm-3.1030.0.tgz#430b86d76add91913b220c2de3234a3af05b1f75" + integrity sha512-FKu4tINBafrEp6FfoJDaM+KvTqwwK5gnVTrc0ZYbAQ5L7oMuCx02MEQvRI6VLaNhuIqXMKijKo2lodyLY+00WA== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-node" "^3.972.30" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-utf8" "^4.2.2" + "@smithy/util-waiter" "^4.2.15" + tslib "^2.6.2" + "@aws-sdk/core@^3.973.26": version "3.973.26" resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.26.tgz#5989c5300f9da7ed57f34b88091c77b4fa5d7256" @@ -568,6 +789,33 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@aws-sdk/core@^3.973.27": + version "3.973.27" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.973.27.tgz#cc2872a8d54357f5bc6d9475400291c653ab5d08" + integrity sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/xml-builder" "^3.972.17" + "@smithy/core" "^3.23.14" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/property-provider" "^4.2.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + +"@aws-sdk/crc64-nvme@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.6.tgz#4e023b3e3b5f67d3129c97c5caa3e18699d3d550" + integrity sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-env@^3.972.24": version "3.972.24" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz#bc33a34f15704d02552aa8b3994d17008b991f86" @@ -579,6 +827,17 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-env@^3.972.25": + version "3.972.25" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz#6a55730ec56597545119e2013101c5872c7b1602" + integrity sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-http@^3.972.26": version "3.972.26" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz#6524c3681dbb62d3c4de82262631ab94b800f00e" @@ -595,6 +854,22 @@ "@smithy/util-stream" "^4.5.21" tslib "^2.6.2" +"@aws-sdk/credential-provider-http@^3.972.27": + version "3.972.27" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz#371cca39c19b52012ec2bf025299a233d26445b2" + integrity sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/property-provider" "^4.2.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/util-stream" "^4.5.22" + tslib "^2.6.2" + "@aws-sdk/credential-provider-ini@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.28.tgz#6bc0d684c245914dca7a1a4dd3c2d84212833320" @@ -615,6 +890,26 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-ini@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz#0129911b1ca5e561b4e25d494447457ee7540eaa" + integrity sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-env" "^3.972.25" + "@aws-sdk/credential-provider-http" "^3.972.27" + "@aws-sdk/credential-provider-login" "^3.972.29" + "@aws-sdk/credential-provider-process" "^3.972.25" + "@aws-sdk/credential-provider-sso" "^3.972.29" + "@aws-sdk/credential-provider-web-identity" "^3.972.29" + "@aws-sdk/nested-clients" "^3.996.19" + "@aws-sdk/types" "^3.973.7" + "@smithy/credential-provider-imds" "^4.2.13" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-login@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.28.tgz#b2d47d4d43690d2d824edc94ce955d86dd3877f1" @@ -629,6 +924,20 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-login@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz#a861534cc0bdec0ce506c6c7310fdd57a4caacc8" + integrity sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/nested-clients" "^3.996.19" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-node@^3.972.29": version "3.972.29" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.29.tgz#4bcc991fcbf245f75494a119b3446a678a51e019" @@ -647,6 +956,24 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-node@^3.972.30": + version "3.972.30" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz#cbf0da21b1fe14108829ed17eaa153fb5fe55c85" + integrity sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.25" + "@aws-sdk/credential-provider-http" "^3.972.27" + "@aws-sdk/credential-provider-ini" "^3.972.29" + "@aws-sdk/credential-provider-process" "^3.972.25" + "@aws-sdk/credential-provider-sso" "^3.972.29" + "@aws-sdk/credential-provider-web-identity" "^3.972.29" + "@aws-sdk/types" "^3.973.7" + "@smithy/credential-provider-imds" "^4.2.13" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-process@^3.972.24": version "3.972.24" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz#940c76a2db0aece23879dcf75ac5b6ee8f8fa135" @@ -659,6 +986,18 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-process@^3.972.25": + version "3.972.25" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz#631bd69f28600a6ef134a4cb6e0395371814d3f4" + integrity sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-sso@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.28.tgz#bf150bfb7e708d58f35bb2b5786b902df19fd92d" @@ -673,6 +1012,20 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-sso@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz#7410169f97f686eaab33daed7e18789a46de1116" + integrity sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/nested-clients" "^3.996.19" + "@aws-sdk/token-providers" "3.1026.0" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-web-identity@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.28.tgz#27fc2a0fe0d2ff1460171d2a6912898c2235a7df" @@ -686,6 +1039,19 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-web-identity@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz#ed3c750076cb9131fd940535ea7e94b846a885dd" + integrity sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/nested-clients" "^3.996.19" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/dynamodb-codec@^3.972.27": version "3.972.27" resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.27.tgz#3d29a2f00bbc145260419878a5f3640af81d36b3" @@ -728,6 +1094,19 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-bucket-endpoint@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.9.tgz#4dc1e7a155e612b447387c268740781c785d5810" + integrity sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-arn-parser" "^3.972.3" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/middleware-endpoint-discovery@^3.972.9": version "3.972.9" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.9.tgz#664f9074b0017255680c200bd9b8b23a864c0ad5" @@ -750,6 +1129,36 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-expect-continue@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.9.tgz#ad62cbc4c5f310a5d104b7fc1150eca13a3c07a4" + integrity sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-flexible-checksums@^3.974.7": + version "3.974.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.7.tgz#cc2c8efc5932e7bb55d58d717fe60c45fbf21a41" + integrity sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@aws-crypto/crc32c" "5.2.0" + "@aws-crypto/util" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/crc64-nvme" "^3.972.6" + "@aws-sdk/types" "^3.973.7" + "@smithy/is-array-buffer" "^4.2.2" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/middleware-host-header@^3.972.8": version "3.972.8" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz#72186e96500b49b38fb5482d6b7bf95e5b985281" @@ -760,6 +1169,25 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-host-header@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz#0a7e66857bcb0ebce1aff1cd0e9eb2fe46069260" + integrity sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-location-constraint@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.9.tgz#35a7a35b678d931970b146024078c509631861ad" + integrity sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/middleware-logger@^3.972.8": version "3.972.8" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz#7fee4223afcb6f7828dbdf4ea745ce15027cf384" @@ -769,6 +1197,26 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-logger@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz#a47610fe11f953718d405ec3b36d807c9f3c8b22" + integrity sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-recursion-detection@^3.972.10": + version "3.972.10" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz#9300b3fa7843f5c353b6be7a3c64a2cf486c3a22" + integrity sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@aws/lambda-invoke-store" "^0.2.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/middleware-recursion-detection@^3.972.9": version "3.972.9" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz#53a2cc0cf827863163b2351209212f642015c2e2" @@ -780,6 +1228,49 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-sdk-ec2@^3.972.19": + version "3.972.19" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.972.19.tgz#9b66499bebae68321e50dbcf3f839faf30d28e60" + integrity sha512-eB73yVCMipYwoxiKzRAy4gt1FiAVl/EodfdMxvPomKZw+yWEWKiGhwrVhtLHhFRAM+QkMLnEslsbvsyFELHW+g== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-format-url" "^3.972.9" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@^3.972.28": + version "3.972.28" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.28.tgz#cfdcaab69da8870e039dc58499ac323cd7667242" + integrity sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-arn-parser" "^3.972.3" + "@smithy/core" "^3.23.14" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + +"@aws-sdk/middleware-ssec@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz#3658fd92752682316c48b736d6c013a75cfcd7aa" + integrity sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/middleware-user-agent@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.28.tgz#7f81d96d2fed0334ff601af62d77e14f67fb9d22" @@ -794,6 +1285,20 @@ "@smithy/util-retry" "^4.2.13" tslib "^2.6.2" +"@aws-sdk/middleware-user-agent@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz#60931e54bf78cfd41bb39e620d86e30bececbf43" + integrity sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@smithy/core" "^3.23.14" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-retry" "^4.3.0" + tslib "^2.6.2" + "@aws-sdk/middleware-websocket@^3.972.14": version "3.972.14" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.14.tgz#52ea3b4fddb4320bd23891a4ce103f193b94cadf" @@ -856,6 +1361,50 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@aws-sdk/nested-clients@^3.996.19": + version "3.996.19" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz#3e43e3154038e33a59917ec5d015d1f438b6af22" + integrity sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/region-config-resolver@^3.972.10": version "3.972.10" resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz#cbabd969a2d4fedb652273403e64d98b79d0144c" @@ -867,6 +1416,29 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/region-config-resolver@^3.972.11": + version "3.972.11" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz#b9e48d6b900b2a525adecd62ce67597ebf330835" + integrity sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/config-resolver" "^4.4.14" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@^3.996.16": + version "3.996.16" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.16.tgz#a078e17caa4b94dad8add2e8b1be6f2362d4c83f" + integrity sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ== + dependencies: + "@aws-sdk/middleware-sdk-s3" "^3.972.28" + "@aws-sdk/types" "^3.973.7" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/token-providers@3.1021.0": version "3.1021.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1021.0.tgz#90905a8def49f90e54a73849e25ad4bcc4dbea2a" @@ -880,6 +1452,19 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/token-providers@3.1026.0": + version "3.1026.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz#af571864ad4ff3ab2a81ce38cc6d2fa58019df70" + integrity sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/nested-clients" "^3.996.19" + "@aws-sdk/types" "^3.973.7" + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.6": version "3.973.6" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.6.tgz#1964a7c01b5cb18befa445998ad1d02f86c5432d" @@ -888,6 +1473,21 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/types@^3.973.7": + version "3.973.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.7.tgz#0dc48b436638d9f19ca52f686912edda2d5d6dee" + integrity sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/util-arn-parser@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz#ed989862bbb172ce16d9e1cd5790e5fe367219c2" + integrity sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA== + dependencies: + tslib "^2.6.2" + "@aws-sdk/util-dynamodb@^3.996.2": version "3.996.2" resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.2.tgz#9521dfe84c031809f8cf2e32f03c58fd8a4bb84f" @@ -906,6 +1506,17 @@ "@smithy/util-endpoints" "^3.3.3" tslib "^2.6.2" +"@aws-sdk/util-endpoints@^3.996.6": + version "3.996.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz#90934298b655d036d0b181b9fc3239629ba25166" + integrity sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-endpoints" "^3.3.4" + tslib "^2.6.2" + "@aws-sdk/util-format-url@^3.972.8": version "3.972.8" resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz#803273f72617edb16b4087bcff2e52d740a26250" @@ -916,6 +1527,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/util-format-url@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz#a52e141dc7b8dcb954460e34fe4a0b9451734d7b" + integrity sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/querystring-builder" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/util-locate-window@^3.0.0": version "3.965.5" resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz#e30e6ff2aff6436209ed42c765dec2d2a48df7c0" @@ -933,6 +1554,16 @@ bowser "^2.11.0" tslib "^2.6.2" +"@aws-sdk/util-user-agent-browser@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz#3fe2f2bf5949d6ccc21c1bcdd75fd79db6cd4d7f" + integrity sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + bowser "^2.11.0" + tslib "^2.6.2" + "@aws-sdk/util-user-agent-node@^3.973.14": version "3.973.14" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.14.tgz#955e50e8222c9861fdf8f273ba8ff8e28ba04a5c" @@ -945,6 +1576,18 @@ "@smithy/util-config-provider" "^4.2.2" tslib "^2.6.2" +"@aws-sdk/util-user-agent-node@^3.973.15": + version "3.973.15" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz#ac4e1a42c89c205d30aa90992171848f8524d490" + integrity sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w== + dependencies: + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/types" "^3.973.7" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/xml-builder@^3.972.16": version "3.972.16" resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz#ea22fe022cf12d12b07f6faf75c4fa214dea00bc" @@ -954,6 +1597,15 @@ fast-xml-parser "5.5.8" tslib "^2.6.2" +"@aws-sdk/xml-builder@^3.972.17": + version "3.972.17" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz#748480460eaf075acaf16804b2c32158cbfe984d" + integrity sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg== + dependencies: + "@smithy/types" "^4.14.0" + fast-xml-parser "5.5.8" + tslib "^2.6.2" + "@aws/durable-execution-sdk-js@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@aws/durable-execution-sdk-js/-/durable-execution-sdk-js-1.1.0.tgz#c32a4a358cc5940414accc13cd9825766299898d" @@ -2568,6 +3220,21 @@ dependencies: "@sinonjs/commons" "^3.0.1" +"@smithy/chunked-blob-reader-native@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz#9e79a80d8d44798e7ce7a8f968cbbbaf5a40d950" + integrity sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw== + dependencies: + "@smithy/util-base64" "^4.3.2" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz#3af48e37b10e5afed478bb31d2b7bc03c81d196c" + integrity sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw== + dependencies: + tslib "^2.6.2" + "@smithy/config-resolver@^4.4.13": version "4.4.13" resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.13.tgz#8bffd41de647ec349b4a74bf02bdd1b32452bacd" @@ -2580,6 +3247,18 @@ "@smithy/util-middleware" "^4.2.12" tslib "^2.6.2" +"@smithy/config-resolver@^4.4.14": + version "4.4.14" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.14.tgz#6803498f1be96d88da3e6d88a244e4ec99fe3174" + integrity sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ== + dependencies: + "@smithy/node-config-provider" "^4.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + tslib "^2.6.2" + "@smithy/core@^3.23.13": version "3.23.13" resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.23.13.tgz#343e0d78b907f463b560d9e50d8ae16456281830" @@ -2596,6 +3275,22 @@ "@smithy/uuid" "^1.1.2" tslib "^2.6.2" +"@smithy/core@^3.23.14": + version "3.23.14" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.23.14.tgz#29c3b6cf771ee8898018a1cc34c0fe3f418468e5" + integrity sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg== + dependencies: + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + "@smithy/uuid" "^1.1.2" + tslib "^2.6.2" + "@smithy/credential-provider-imds@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz#fa2e52116cac7eaf5625e0bfd399a4927b598f66" @@ -2607,6 +3302,17 @@ "@smithy/url-parser" "^4.2.12" tslib "^2.6.2" +"@smithy/credential-provider-imds@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz#c0533f362dec6644f403c7789d8e81233f78c63f" + integrity sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ== + dependencies: + "@smithy/node-config-provider" "^4.3.13" + "@smithy/property-provider" "^4.2.13" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + tslib "^2.6.2" + "@smithy/eventstream-codec@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz#8cd62d08709344fb8b35fd17870fdf1435de61a3" @@ -2617,6 +3323,16 @@ "@smithy/util-hex-encoding" "^4.2.2" tslib "^2.6.2" +"@smithy/eventstream-codec@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz#7fcdf18bc1acaec395b5d387d65136973bd3e1cc" + integrity sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@smithy/types" "^4.14.0" + "@smithy/util-hex-encoding" "^4.2.2" + tslib "^2.6.2" + "@smithy/eventstream-serde-browser@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz#3ceb8743750edaf5d6e42cd1a2327e048f85ba4e" @@ -2626,6 +3342,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-browser@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz#3b7f4fe380e022db489ca5eef0291b3835c369e6" + integrity sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg== + dependencies: + "@smithy/eventstream-serde-universal" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/eventstream-serde-config-resolver@^4.3.12": version "4.3.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz#a29164bc5480d935ece9dbdca0f79924259e519a" @@ -2634,6 +3359,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-config-resolver@^4.3.13": + version "4.3.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz#75477c75a5d8d4f2844319ba713b345a8b1615e0" + integrity sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/eventstream-serde-node@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz#2cc06a1ea1108f679d376aab81e95a6f69877b4a" @@ -2643,6 +3376,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-node@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz#6ac8f2b06355ba15a3ccf84fc053fff9bd741e35" + integrity sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A== + dependencies: + "@smithy/eventstream-serde-universal" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/eventstream-serde-universal@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz#a3640d1e7c3e348168360035661db8d21b51e078" @@ -2652,6 +3394,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-universal@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz#805c5dfea13bcffb72e3ea46a03de43ddb70843b" + integrity sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA== + dependencies: + "@smithy/eventstream-codec" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/fetch-http-handler@^5.3.15": version "5.3.15" resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz#acf69a8b3bab0396d2782fc901bad0b957c8c6a2" @@ -2663,6 +3414,27 @@ "@smithy/util-base64" "^4.3.2" tslib "^2.6.2" +"@smithy/fetch-http-handler@^5.3.16": + version "5.3.16" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz#2cd94de19ac2bcdb51682259cf6dcacbb1b382a9" + integrity sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ== + dependencies: + "@smithy/protocol-http" "^5.3.13" + "@smithy/querystring-builder" "^4.2.13" + "@smithy/types" "^4.14.0" + "@smithy/util-base64" "^4.3.2" + tslib "^2.6.2" + +"@smithy/hash-blob-browser@^4.2.14": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.14.tgz#c32a6a5b70fa94e324f2ca04296e2355ddfe4c9b" + integrity sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA== + dependencies: + "@smithy/chunked-blob-reader" "^5.2.2" + "@smithy/chunked-blob-reader-native" "^4.2.3" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/hash-node@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.2.12.tgz#0ee7f6a1d2958c313ee24b07159dcb9547792441" @@ -2673,6 +3445,25 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@smithy/hash-node@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.2.13.tgz#5ec1b80c27f5446136ce98bf6ab0b0594ca34511" + integrity sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA== + dependencies: + "@smithy/types" "^4.14.0" + "@smithy/util-buffer-from" "^4.2.2" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + +"@smithy/hash-stream-node@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-4.2.13.tgz#0e0912b12b8f11c360446812e2ada8fec3f6ddd1" + integrity sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg== + dependencies: + "@smithy/types" "^4.14.0" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/invalid-dependency@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz#1a28c13fb33684b91848d4d6ec5104a1c1413e7f" @@ -2681,6 +3472,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/invalid-dependency@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz#0f23859d529ba669f24860baacb41835f604a8ae" + integrity sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/is-array-buffer@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" @@ -2695,6 +3494,15 @@ dependencies: tslib "^2.6.2" +"@smithy/md5-js@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-4.2.13.tgz#4c96c41336d7d655758c3a7457439fabc9d4b6cd" + integrity sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ== + dependencies: + "@smithy/types" "^4.14.0" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/middleware-content-length@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz#dec97ea1444b12e734156b764e9953b2b37c70fd" @@ -2704,6 +3512,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/middleware-content-length@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz#0bbc3706fe1321ba99be29703ff98abde996d49d" + integrity sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig== + dependencies: + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/middleware-endpoint@^4.4.28": version "4.4.28" resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz#201b568f3669bd816f60a6043d914c134d80f46c" @@ -2718,6 +3535,20 @@ "@smithy/util-middleware" "^4.2.12" tslib "^2.6.2" +"@smithy/middleware-endpoint@^4.4.29": + version "4.4.29" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz#86fa2f206469e48bff1b30b2c35e433b5f453119" + integrity sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw== + dependencies: + "@smithy/core" "^3.23.14" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-middleware" "^4.2.13" + tslib "^2.6.2" + "@smithy/middleware-retry@^4.4.46": version "4.4.46" resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.46.tgz#dbbf0af08c1bd03fe2afa09a6cfb7a9056387ce6" @@ -2733,6 +3564,22 @@ "@smithy/uuid" "^1.1.2" tslib "^2.6.2" +"@smithy/middleware-retry@^4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.5.0.tgz#d39bec675ba3133f399c21261212d690f1e10d61" + integrity sha512-/NzISn4grj/BRFVua/xnQwF+7fakYZgimpw2dfmlPgcqecBMKxpB9g5mLYRrmBD5OrPoODokw4Vi1hrSR4zRyw== + dependencies: + "@smithy/core" "^3.23.14" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/service-error-classification" "^4.2.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/uuid" "^1.1.2" + tslib "^2.6.2" + "@smithy/middleware-serde@^4.2.16": version "4.2.16" resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz#7f259e1e4e43332ad29b53cf3b4d9f14fde690ce" @@ -2743,6 +3590,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/middleware-serde@^4.2.17": + version "4.2.17" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz#45b1eaa99c3b536042eb56365096e6681f2a347b" + integrity sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ== + dependencies: + "@smithy/core" "^3.23.14" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/middleware-stack@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz#96b43b2fab0d4a6723f813f76b72418b0fdb6ba0" @@ -2751,6 +3608,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/middleware-stack@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz#88007ea7eb40ab3ff632701c21149e0e8a57b55f" + integrity sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/node-config-provider@^4.3.12": version "4.3.12" resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz#bb722da6e2a130ae585754fa7bc8d909f9f5d702" @@ -2761,6 +3626,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/node-config-provider@^4.3.13": + version "4.3.13" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz#a65c696a38a0c2e7012652b1c1138799882b12bc" + integrity sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw== + dependencies: + "@smithy/property-provider" "^4.2.13" + "@smithy/shared-ini-file-loader" "^4.4.8" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/node-http-handler@^4.5.1": version "4.5.1" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz#9f05b4478ccfc6db82af37579a36fa48ee8f6067" @@ -2771,6 +3646,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/node-http-handler@^4.5.2": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz#21d70f4c9cf1ce59921567bab59ae1177b6c60b1" + integrity sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA== + dependencies: + "@smithy/protocol-http" "^5.3.13" + "@smithy/querystring-builder" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/property-provider@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.12.tgz#e9f8e5ce125413973b16e39c87cf4acd41324e21" @@ -2779,6 +3664,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/property-provider@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.13.tgz#4859f887414f2c251517125258870a70509f8bbd" + integrity sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/protocol-http@^5.3.12": version "5.3.12" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.12.tgz#c913053e7dfbac6cdd7f374f0b4f5aa7c518d0e1" @@ -2787,6 +3680,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/protocol-http@^5.3.13": + version "5.3.13" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.13.tgz#1e8fcacd61282cafc2c783ab002cb0debe763588" + integrity sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/querystring-builder@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz#20a0266b151a4b58409f901e1463257a72835c16" @@ -2796,6 +3697,15 @@ "@smithy/util-uri-escape" "^4.2.2" tslib "^2.6.2" +"@smithy/querystring-builder@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz#1f3c009493a06d83f998da70f5920246dfcd88dd" + integrity sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ== + dependencies: + "@smithy/types" "^4.14.0" + "@smithy/util-uri-escape" "^4.2.2" + tslib "^2.6.2" + "@smithy/querystring-parser@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz#918cb609b2d606ab81f2727bfde0265d2ebb2758" @@ -2804,6 +3714,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/querystring-parser@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz#c2ab4446a50d0de232bbffdab534b3e0023bf879" + integrity sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/service-error-classification@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz#795e9484207acf63817a9e9cf67e90b42e720840" @@ -2811,6 +3729,13 @@ dependencies: "@smithy/types" "^4.13.1" +"@smithy/service-error-classification@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz#22aa256bbad30d98e13a4896eee165ee184cd33b" + integrity sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw== + dependencies: + "@smithy/types" "^4.14.0" + "@smithy/shared-ini-file-loader@^4.4.7": version "4.4.7" resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz#18cc5a21f871509fafbe535a7bf44bde5a500727" @@ -2819,6 +3744,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/shared-ini-file-loader@^4.4.8": + version "4.4.8" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz#c45099e8aea8f48af97d05be91ab6ae93d105ae7" + integrity sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/signature-v4@^5.3.12": version "5.3.12" resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.12.tgz#b61ce40a94bdd91dfdd8f5f2136631c8eb67f253" @@ -2833,6 +3766,20 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@smithy/signature-v4@^5.3.13": + version "5.3.13" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.13.tgz#0c3760a5837673ddbb66c433637d5e16742b991f" + integrity sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg== + dependencies: + "@smithy/is-array-buffer" "^4.2.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-hex-encoding" "^4.2.2" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-uri-escape" "^4.2.2" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/smithy-client@^4.12.8": version "4.12.8" resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.12.8.tgz#b2982fe8b72e44621c139045d991555c07df0e1a" @@ -2846,6 +3793,19 @@ "@smithy/util-stream" "^4.5.21" tslib "^2.6.2" +"@smithy/smithy-client@^4.12.9": + version "4.12.9" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.12.9.tgz#2eb54ee07050a8bcd3792f8b8c4e03fac4bfb422" + integrity sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ== + dependencies: + "@smithy/core" "^3.23.14" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-stream" "^4.5.22" + tslib "^2.6.2" + "@smithy/types@^4.13.1": version "4.13.1" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.13.1.tgz#8aaf15bb0f42b4e7c93c87018a3678a06d74691d" @@ -2853,6 +3813,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.14.0.tgz#72fb6fd315f2eff7d4878142db2d1db4ef94f9bc" + integrity sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ== + dependencies: + tslib "^2.6.2" + "@smithy/url-parser@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.2.12.tgz#e940557bf0b8e9a25538a421970f64bd827f456f" @@ -2862,6 +3829,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/url-parser@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.2.13.tgz#cc582733d1181e1a135b05bb600f12c9889be7f4" + integrity sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw== + dependencies: + "@smithy/querystring-parser" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-base64@^4.3.2": version "4.3.2" resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-4.3.2.tgz#be02bcb29a87be744356467ea25ffa413e695cea" @@ -2918,6 +3894,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-defaults-mode-browser@^4.3.45": + version "4.3.45" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz#42cb7fb97857a6b67d54e38adaf1476fdc7d1339" + integrity sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw== + dependencies: + "@smithy/property-provider" "^4.2.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-defaults-mode-node@^4.2.48": version "4.2.48" resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz#8ee63e2ea706bd111104e8f3796d858cc186625f" @@ -2931,6 +3917,19 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-defaults-mode-node@^4.2.49": + version "4.2.49" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.49.tgz#fa443a16daedef503c0d41bbed22526c3e228cee" + integrity sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ== + dependencies: + "@smithy/config-resolver" "^4.4.14" + "@smithy/credential-provider-imds" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/property-provider" "^4.2.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-endpoints@^3.3.3": version "3.3.3" resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz#0119f15bcac30b3b9af1d3cc0a8477e7199d0185" @@ -2940,6 +3939,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-endpoints@^3.3.4": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.3.4.tgz#e372596c9aebd7939a0452f6b8ec417cfac18f7c" + integrity sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog== + dependencies: + "@smithy/node-config-provider" "^4.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-hex-encoding@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz#4abf3335dd1eb884041d8589ca7628d81a6fd1d3" @@ -2955,6 +3963,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-middleware@^4.2.13": + version "4.2.13" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.13.tgz#fda5518f95cc3f4a3086d9ee46cc42797baaedf8" + integrity sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-retry@^4.2.13": version "4.2.13" resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.13.tgz#ad816d6ddf197095d188e9ef56664fbd392a39c9" @@ -2964,6 +3980,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-retry@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.3.0.tgz#efff6f9859ddfeb7747b269cf236f47c4bc2a54d" + integrity sha512-tSOPQNT/4KfbvqeMovWC3g23KSYy8czHd3tlN+tOYVNIDLSfxIsrPJihYi5TpNcoV789KWtgChUVedh2y6dDPg== + dependencies: + "@smithy/service-error-classification" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/util-stream@^4.5.21": version "4.5.21" resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.21.tgz#a9ea13d0299d030c72ab4b4e394db111cd581629" @@ -2978,6 +4003,20 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@smithy/util-stream@^4.5.22": + version "4.5.22" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.5.22.tgz#16e449bbd174243b9e202f0f75d33a1d700c2020" + integrity sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew== + dependencies: + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/types" "^4.14.0" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-buffer-from" "^4.2.2" + "@smithy/util-hex-encoding" "^4.2.2" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/util-uri-escape@^4.2.2": version "4.2.2" resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz#48e40206e7fe9daefc8d44bb43a1ab17e76abf4a" @@ -3009,6 +4048,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/util-waiter@^4.2.15": + version "4.2.15" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-4.2.15.tgz#0338ad7e5b47380836cfedd21a6b5bda4e43a88f" + integrity sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@smithy/uuid@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@smithy/uuid/-/uuid-1.1.2.tgz#b6e97c7158615e4a3c775e809c00d8c269b5a12e"