From bad88672e4ef9d32f475f22d79cd7a8cf514e7ba Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Tue, 7 Apr 2026 08:57:28 -0700 Subject: [PATCH 1/8] feat(orchestrator): extract ComputeStrategy interface from hardcoded AgentCore logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce ComputeStrategy interface with SessionHandle/SessionStatus types and resolveComputeStrategy factory. Extract AgentCoreComputeStrategy from orchestrator.ts. Refactor orchestrate-task handler to use strategy pattern for session lifecycle (start/poll/stop). Pure refactor — no behavior change, identical CloudFormation output. --- cdk/src/handlers/orchestrate-task.ts | 19 ++- cdk/src/handlers/shared/compute-strategy.ts | 53 +++++++ cdk/src/handlers/shared/orchestrator.ts | 40 ------ .../shared/strategies/agentcore-strategy.ts | 90 ++++++++++++ cdk/test/handlers/orchestrate-task.test.ts | 32 +---- .../handlers/shared/compute-strategy.test.ts | 47 +++++++ .../strategies/agentcore-strategy.test.ts | 129 ++++++++++++++++++ 7 files changed, 337 insertions(+), 73 deletions(-) create mode 100644 cdk/src/handlers/shared/compute-strategy.ts create mode 100644 cdk/src/handlers/shared/strategies/agentcore-strategy.ts create mode 100644 cdk/test/handlers/shared/compute-strategy.test.ts create mode 100644 cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts diff --git a/cdk/src/handlers/orchestrate-task.ts b/cdk/src/handlers/orchestrate-task.ts index e1f066c..beca6d9 100644 --- a/cdk/src/handlers/orchestrate-task.ts +++ b/cdk/src/handlers/orchestrate-task.ts @@ -19,6 +19,7 @@ import { withDurableExecution, type DurableExecutionHandler } from '@aws/durable-execution-sdk-js'; import { TaskStatus, TERMINAL_STATUSES } from '../constructs/task-status'; +import { resolveComputeStrategy } from './shared/compute-strategy'; import { admissionControl, emitTaskEvent, @@ -28,7 +29,7 @@ import { loadBlueprintConfig, loadTask, pollTaskStatus, - startSession, + transitionTask, type PollState, } from './shared/orchestrator'; import { runPreflightChecks } from './shared/preflight'; @@ -116,10 +117,22 @@ const durableHandler: DurableExecutionHandler = asyn } }); - // Step 4: Start agent session — invoke runtime and transition to RUNNING + // Step 4: Start agent session — resolve compute strategy, invoke runtime, transition to RUNNING 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 }); + + await transitionTask(taskId, TaskStatus.HYDRATING, TaskStatus.RUNNING, { + session_id: handle.sessionId, + started_at: new Date().toISOString(), + }); + await emitTaskEvent(taskId, 'session_started', { + session_id: handle.sessionId, + strategy_type: handle.strategyType, + }); + + return handle.sessionId; } catch (err) { await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, task.user_id, true); throw err; diff --git a/cdk/src/handlers/shared/compute-strategy.ts b/cdk/src/handlers/shared/compute-strategy.ts new file mode 100644 index 0000000..c4465f0 --- /dev/null +++ b/cdk/src/handlers/shared/compute-strategy.ts @@ -0,0 +1,53 @@ +/** + * 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 } from './repo-config'; +import { AgentCoreComputeStrategy } from './strategies/agentcore-strategy'; + +export interface SessionHandle { + readonly sessionId: string; + readonly strategyType: string; + readonly metadata: Record; +} + +export type SessionStatus = + | { readonly status: 'running' } + | { readonly status: 'completed' } + | { readonly status: 'failed'; readonly error: string }; + +export interface ComputeStrategy { + readonly type: string; + 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 = blueprintConfig.compute_type; + switch (computeType) { + case 'agentcore': + return new AgentCoreComputeStrategy({ runtimeArn: blueprintConfig.runtime_arn }); + default: + throw new Error(`Unknown compute_type: '${computeType}'`); + } +} diff --git a/cdk/src/handlers/shared/orchestrator.ts b/cdk/src/handlers/shared/orchestrator.ts index 2f4bcc6..d4a14a7 100644 --- a/cdk/src/handlers/shared/orchestrator.ts +++ b/cdk/src/handlers/shared/orchestrator.ts @@ -17,8 +17,6 @@ * SOFTWARE. */ -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'; @@ -32,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!; @@ -305,43 +302,6 @@ export async function hydrateAndTransition(task: TaskRecord, blueprintConfig?: B return payload; } -/** - * Start an AgentCore runtime session and transition task to RUNNING. - * @param task - the task record. - * @param payload - the hydrated invocation payload. - * @param blueprintConfig - optional per-repo blueprint config for runtime ARN override. - * @returns the session ID. - */ -export async function startSession( - task: TaskRecord, - payload: Record, - 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(), - }); - 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/strategies/agentcore-strategy.ts b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts new file mode 100644 index 0000000..92ce335 --- /dev/null +++ b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts @@ -0,0 +1,90 @@ +/** + * 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'; + +export class AgentCoreComputeStrategy implements ComputeStrategy { + readonly type = 'agentcore'; + private readonly client: BedrockAgentCoreClient; + private readonly runtimeArn: string; + + constructor(options: { runtimeArn: string }) { + this.runtimeArn = options.runtimeArn; + this.client = new BedrockAgentCoreClient({}); + } + + 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 ?? this.runtimeArn; + + const command = new InvokeAgentRuntimeCommand({ + agentRuntimeArn: runtimeArn, + runtimeSessionId: sessionId, + contentType: 'application/json', + accept: 'application/json', + payload: new TextEncoder().encode(JSON.stringify({ input: input.payload })), + }); + + await this.client.send(command); + + logger.info('AgentCore session invoked', { task_id: input.taskId, session_id: sessionId, runtime_arn: runtimeArn }); + + return { + sessionId, + strategyType: this.type, + metadata: { runtimeArn }, + }; + } + + async pollSession(_handle: SessionHandle): Promise { + // Polling is currently done at the orchestrator level via DDB reads. + // This method exists for PR 2 where different strategies may poll differently. + return { status: 'running' }; + } + + async stopSession(handle: SessionHandle): Promise { + const runtimeArn = handle.metadata.runtimeArn as string | undefined; + if (!runtimeArn) { + logger.warn('No runtimeArn in session handle, cannot stop session', { session_id: handle.sessionId }); + return; + } + + try { + await this.client.send(new StopRuntimeSessionCommand({ + agentRuntimeArn: runtimeArn, + runtimeSessionId: handle.sessionId, + })); + logger.info('AgentCore session stopped', { session_id: handle.sessionId }); + } catch (err) { + 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/test/handlers/orchestrate-task.test.ts b/cdk/test/handlers/orchestrate-task.test.ts index dfa1d39..c78ffbb 100644 --- a/cdk/test/handlers/orchestrate-task.test.ts +++ b/cdk/test/handlers/orchestrate-task.test.ts @@ -27,10 +27,10 @@ jest.mock('@aws-sdk/lib-dynamodb', () => ({ UpdateCommand: jest.fn((input: unknown) => ({ _type: 'Update', input })), })); -const mockAgentCoreSend = jest.fn(); jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ - BedrockAgentCoreClient: jest.fn(() => ({ send: mockAgentCoreSend })), + BedrockAgentCoreClient: jest.fn(() => ({ send: jest.fn() })), InvokeAgentRuntimeCommand: jest.fn((input: unknown) => ({ _type: 'InvokeAgentRuntime', input })), + StopRuntimeSessionCommand: jest.fn((input: unknown) => ({ _type: 'StopRuntimeSession', input })), })); const mockHydrateContext = jest.fn(); @@ -75,7 +75,6 @@ import { loadBlueprintConfig, loadTask, pollTaskStatus, - startSession, transitionTask, } from '../../src/handlers/shared/orchestrator'; @@ -192,18 +191,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); - }); -}); - describe('pollTaskStatus', () => { test('increments attempt count and reads status', async () => { mockDdbSend.mockResolvedValueOnce({ Item: { status: 'RUNNING' } }); @@ -385,21 +372,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..7013dd6 --- /dev/null +++ b/cdk/test/handlers/shared/compute-strategy.test.ts @@ -0,0 +1,47 @@ +/** + * 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(), +})); + +import { resolveComputeStrategy } from '../../../src/handlers/shared/compute-strategy'; +import { AgentCoreComputeStrategy } from '../../../src/handlers/shared/strategies/agentcore-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('throws for unknown compute_type', () => { + expect(() => + resolveComputeStrategy({ + compute_type: 'unknown', + runtime_arn: 'arn:test', + }), + ).toThrow("Unknown compute_type: 'unknown'"); + }); +}); 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..1359248 --- /dev/null +++ b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts @@ -0,0 +1,129 @@ +/** + * 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 { AgentCoreComputeStrategy } from '../../../../src/handlers/shared/strategies/agentcore-strategy'; + +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({ runtimeArn: defaultRuntimeArn }); + expect(strategy.type).toBe('agentcore'); + }); + + describe('startSession', () => { + test('invokes agent runtime and returns SessionHandle', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + + 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'); + expect(handle.metadata.runtimeArn).toBe(defaultRuntimeArn); + expect(mockSend).toHaveBeenCalledTimes(1); + }); + + test('uses blueprint runtime_arn override', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + const overrideArn = '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: overrideArn }, + }); + + expect(handle.metadata.runtimeArn).toBe(overrideArn); + const invokeCall = mockSend.mock.calls[0][0]; + expect(invokeCall.input.agentRuntimeArn).toBe(overrideArn); + }); + }); + + describe('pollSession', () => { + test('returns running status', async () => { + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + const result = await strategy.pollSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + metadata: { runtimeArn: defaultRuntimeArn }, + }); + expect(result.status).toBe('running'); + }); + }); + + describe('stopSession', () => { + test('sends StopRuntimeSessionCommand', async () => { + mockSend.mockResolvedValueOnce({}); + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + + await strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + metadata: { 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('does not throw when stop fails', async () => { + mockSend.mockRejectedValueOnce(new Error('Access denied')); + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + + await expect( + strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + metadata: { runtimeArn: defaultRuntimeArn }, + }), + ).resolves.toBeUndefined(); + }); + + test('skips stop when no runtimeArn in metadata', async () => { + const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + + await strategy.stopSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + metadata: {}, + }); + + expect(mockSend).not.toHaveBeenCalled(); + }); + }); +}); From 1a1d2907d4be37bc3088c8c580fef1e6d49105a9 Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Tue, 7 Apr 2026 10:28:56 -0700 Subject: [PATCH 2/8] fix(ci): pass GITHUB_TOKEN to mise to avoid API rate limits The mise install step downloads tools (trivy) from GitHub releases. Without GITHUB_TOKEN, unauthenticated requests hit the 60 req/hr rate limit, causing flaky CI failures. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db9cf55..6c7312e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,7 @@ jobs: env: CI: "true" MISE_EXPERIMENTAL: "1" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v5 From 93a2bfa85493393e8668273eda2272d0aa05b29d Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Tue, 7 Apr 2026 10:32:36 -0700 Subject: [PATCH 3/8] fix(ci): set GITHUB_API_TOKEN for mise tool downloads Mise uses GITHUB_API_TOKEN (not GITHUB_TOKEN) for authenticated GitHub API requests when downloading aqua tools like trivy. --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c7312e..dbb95e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,7 @@ jobs: CI: "true" MISE_EXPERIMENTAL: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v5 @@ -39,6 +40,8 @@ jobs: uses: jdx/mise-action@v3.6.2 with: cache: true + env: + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v5 with: From 41e2e2d60134b7d6a580622ccdb8668bb2ed8b95 Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Tue, 7 Apr 2026 10:38:14 -0700 Subject: [PATCH 4/8] fix(ci): disable security-only tools in build workflow Trivy, grype, semgrep, osv-scanner, and gitleaks are only needed for security scanning tasks, not for the build/test/synth pipeline. Disable them via MISE_DISABLE_TOOLS to avoid GitHub API rate limits when mise tries to download them on every PR build. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dbb95e7..30aa8a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,8 @@ jobs: MISE_EXPERIMENTAL: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AQUA_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_DISABLE_TOOLS: "aqua:aquasecurity/trivy,grype,semgrep,osv-scanner,gitleaks" steps: - name: Checkout uses: actions/checkout@v5 @@ -40,8 +42,6 @@ jobs: uses: jdx/mise-action@v3.6.2 with: cache: true - env: - GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v5 with: From eba70ca7a2b6274ae870bee9783577e1f64a235b Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Wed, 8 Apr 2026 10:56:12 -0700 Subject: [PATCH 5/8] fix: address PR review comments - Keep gitleaks and osv-scanner enabled in CI build (only disable trivy/grype/semgrep which need GitHub API downloads) - Remove unused @aws-sdk/client-bedrock-agentcore mock from orchestrate-task.test.ts (SDK is no longer imported by orchestrator) - Update PR description to note additive strategy_type event field --- .github/workflows/build.yml | 4 +++- cdk/test/handlers/orchestrate-task.test.ts | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30aa8a1..3d05dd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} AQUA_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_DISABLE_TOOLS: "aqua:aquasecurity/trivy,grype,semgrep,osv-scanner,gitleaks" + # Security-only tools not needed for build/test/synth; disable to avoid + # GitHub API rate limits when mise downloads them on every PR. + MISE_DISABLE_TOOLS: "aqua:aquasecurity/trivy,grype,semgrep" steps: - name: Checkout uses: actions/checkout@v5 diff --git a/cdk/test/handlers/orchestrate-task.test.ts b/cdk/test/handlers/orchestrate-task.test.ts index c78ffbb..bc9088e 100644 --- a/cdk/test/handlers/orchestrate-task.test.ts +++ b/cdk/test/handlers/orchestrate-task.test.ts @@ -27,12 +27,6 @@ jest.mock('@aws-sdk/lib-dynamodb', () => ({ UpdateCommand: jest.fn((input: unknown) => ({ _type: 'Update', input })), })); -jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ - BedrockAgentCoreClient: jest.fn(() => ({ send: jest.fn() })), - InvokeAgentRuntimeCommand: jest.fn((input: unknown) => ({ _type: 'InvokeAgentRuntime', input })), - StopRuntimeSessionCommand: jest.fn((input: unknown) => ({ _type: 'StopRuntimeSession', input })), -})); - const mockHydrateContext = jest.fn(); jest.mock('../../src/handlers/shared/context-hydration', () => ({ hydrateContext: mockHydrateContext, From d68edd6af5e6a90d9dcba0e1125ef441d6671371 Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Wed, 8 Apr 2026 11:21:16 -0700 Subject: [PATCH 6/8] fix: address Alain's PR review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Single source of truth for runtimeArn — removed constructor param, strategy now reads exclusively from blueprintConfig.runtime_arn 2. Lazy singleton for BedrockAgentCoreClient — module-level shared client avoids creating new TLS sessions per invocation 3. ComputeType union type ('agentcore' | 'ecs') with exhaustive switch and never-pattern in resolveComputeStrategy 4. Differentiated error handling in stopSession — ResourceNotFoundException (info), ThrottlingException/AccessDeniedException (error), others (warn) 5. Added logger.info('Session started') after full invoke+transition+event sequence in orchestrate-task.ts 6. Added start-session-composition.test.ts with integration tests for happy path, error path (failTask), and partial failure (transitionTask throws) 7. pollSession now throws NotImplementedError instead of returning stale 'running' status — clear signal for future developers --- cdk/src/handlers/orchestrate-task.ts | 7 + cdk/src/handlers/shared/compute-strategy.ts | 14 +- cdk/src/handlers/shared/repo-config.ts | 6 +- .../shared/strategies/agentcore-strategy.ts | 48 ++++-- .../handlers/shared/compute-strategy.test.ts | 6 +- cdk/test/handlers/shared/preflight.test.ts | 2 +- .../strategies/agentcore-strategy.test.ts | 115 ++++++++++--- .../start-session-composition.test.ts | 158 ++++++++++++++++++ 8 files changed, 307 insertions(+), 49 deletions(-) create mode 100644 cdk/test/handlers/start-session-composition.test.ts diff --git a/cdk/src/handlers/orchestrate-task.ts b/cdk/src/handlers/orchestrate-task.ts index beca6d9..0fcb757 100644 --- a/cdk/src/handlers/orchestrate-task.ts +++ b/cdk/src/handlers/orchestrate-task.ts @@ -20,6 +20,7 @@ import { withDurableExecution, type DurableExecutionHandler } from '@aws/durable-execution-sdk-js'; import { TaskStatus, TERMINAL_STATUSES } from '../constructs/task-status'; import { resolveComputeStrategy } from './shared/compute-strategy'; +import { logger } from './shared/logger'; import { admissionControl, emitTaskEvent, @@ -132,6 +133,12 @@ const durableHandler: DurableExecutionHandler = asyn strategy_type: handle.strategyType, }); + logger.info('Session started', { + task_id: taskId, + session_id: handle.sessionId, + strategy_type: handle.strategyType, + }); + return handle.sessionId; } catch (err) { await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, task.user_id, true); diff --git a/cdk/src/handlers/shared/compute-strategy.ts b/cdk/src/handlers/shared/compute-strategy.ts index c4465f0..5bf2093 100644 --- a/cdk/src/handlers/shared/compute-strategy.ts +++ b/cdk/src/handlers/shared/compute-strategy.ts @@ -17,7 +17,7 @@ * SOFTWARE. */ -import type { BlueprintConfig } from './repo-config'; +import type { BlueprintConfig, ComputeType } from './repo-config'; import { AgentCoreComputeStrategy } from './strategies/agentcore-strategy'; export interface SessionHandle { @@ -43,11 +43,15 @@ export interface ComputeStrategy { } export function resolveComputeStrategy(blueprintConfig: BlueprintConfig): ComputeStrategy { - const computeType = blueprintConfig.compute_type; + const computeType: ComputeType = blueprintConfig.compute_type; switch (computeType) { case 'agentcore': - return new AgentCoreComputeStrategy({ runtimeArn: blueprintConfig.runtime_arn }); - default: - throw new Error(`Unknown compute_type: '${computeType}'`); + return new AgentCoreComputeStrategy(); + case 'ecs': + throw new Error("compute_type 'ecs' is not yet implemented"); + default: { + const _exhaustive: never = computeType; + throw new Error(`Unknown compute_type: '${_exhaustive}'`); + } } } diff --git a/cdk/src/handlers/shared/repo-config.ts b/cdk/src/handlers/shared/repo-config.ts index 5376063..2e05f7e 100644 --- a/cdk/src/handlers/shared/repo-config.ts +++ b/cdk/src/handlers/shared/repo-config.ts @@ -25,12 +25,14 @@ 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'; + 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 model_id?: string; readonly max_turns?: number; @@ -46,7 +48,7 @@ export interface RepoConfig { * settings with platform defaults. */ export interface BlueprintConfig { - readonly compute_type: string; + readonly compute_type: ComputeType; readonly runtime_arn: string; readonly model_id?: string; readonly max_turns?: number; diff --git a/cdk/src/handlers/shared/strategies/agentcore-strategy.ts b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts index 92ce335..689e468 100644 --- a/cdk/src/handlers/shared/strategies/agentcore-strategy.ts +++ b/cdk/src/handlers/shared/strategies/agentcore-strategy.ts @@ -23,15 +23,16 @@ import type { ComputeStrategy, SessionHandle, SessionStatus } from '../compute-s 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'; - private readonly client: BedrockAgentCoreClient; - private readonly runtimeArn: string; - - constructor(options: { runtimeArn: string }) { - this.runtimeArn = options.runtimeArn; - this.client = new BedrockAgentCoreClient({}); - } async startSession(input: { taskId: string; @@ -40,7 +41,7 @@ export class AgentCoreComputeStrategy implements ComputeStrategy { }): Promise { // AgentCore requires runtimeSessionId >= 33 chars; UUID v4 is 36 chars. const sessionId = randomUUID(); - const runtimeArn = input.blueprintConfig.runtime_arn ?? this.runtimeArn; + const runtimeArn = input.blueprintConfig.runtime_arn; const command = new InvokeAgentRuntimeCommand({ agentRuntimeArn: runtimeArn, @@ -50,7 +51,7 @@ export class AgentCoreComputeStrategy implements ComputeStrategy { payload: new TextEncoder().encode(JSON.stringify({ input: input.payload })), }); - await this.client.send(command); + await getClient().send(command); logger.info('AgentCore session invoked', { task_id: input.taskId, session_id: sessionId, runtime_arn: runtimeArn }); @@ -61,10 +62,12 @@ export class AgentCoreComputeStrategy implements ComputeStrategy { }; } + /** + * Not implemented for AgentCore — polling is done at the orchestrator level via DDB reads. + * Future strategies (e.g. ECS/Fargate) will implement compute-level polling here. + */ async pollSession(_handle: SessionHandle): Promise { - // Polling is currently done at the orchestrator level via DDB reads. - // This method exists for PR 2 where different strategies may poll differently. - return { status: 'running' }; + throw new Error('pollSession is not implemented for AgentCore — use orchestrator-level DDB polling'); } async stopSession(handle: SessionHandle): Promise { @@ -75,16 +78,27 @@ export class AgentCoreComputeStrategy implements ComputeStrategy { } try { - await this.client.send(new StopRuntimeSessionCommand({ + await getClient().send(new StopRuntimeSessionCommand({ agentRuntimeArn: runtimeArn, runtimeSessionId: handle.sessionId, })); logger.info('AgentCore session stopped', { session_id: handle.sessionId }); } catch (err) { - logger.warn('Failed to stop AgentCore session (best-effort)', { - session_id: handle.sessionId, - error: err instanceof Error ? err.message : String(err), - }); + const errName = err instanceof Error ? (err as Error & { name?: string }).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/test/handlers/shared/compute-strategy.test.ts b/cdk/test/handlers/shared/compute-strategy.test.ts index 7013dd6..92f589c 100644 --- a/cdk/test/handlers/shared/compute-strategy.test.ts +++ b/cdk/test/handlers/shared/compute-strategy.test.ts @@ -36,12 +36,12 @@ describe('resolveComputeStrategy', () => { expect(strategy.type).toBe('agentcore'); }); - test('throws for unknown compute_type', () => { + test("throws 'not yet implemented' for compute_type ecs", () => { expect(() => resolveComputeStrategy({ - compute_type: 'unknown', + compute_type: 'ecs', runtime_arn: 'arn:test', }), - ).toThrow("Unknown compute_type: 'unknown'"); + ).toThrow("compute_type 'ecs' is not yet implemented"); }); }); diff --git a/cdk/test/handlers/shared/preflight.test.ts b/cdk/test/handlers/shared/preflight.test.ts index 9658c4b..d3add1e 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 index 1359248..0310f3c 100644 --- a/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts +++ b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts @@ -34,14 +34,14 @@ beforeEach(() => { describe('AgentCoreComputeStrategy', () => { test('type is agentcore', () => { - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + 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({ runtimeArn: defaultRuntimeArn }); + const strategy = new AgentCoreComputeStrategy(); const handle = await strategy.startSession({ taskId: 'TASK001', @@ -55,39 +55,67 @@ describe('AgentCoreComputeStrategy', () => { expect(mockSend).toHaveBeenCalledTimes(1); }); - test('uses blueprint runtime_arn override', async () => { + test('uses runtime_arn from blueprintConfig (single source of truth)', async () => { mockSend.mockResolvedValueOnce({}); - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); - const overrideArn = 'arn:aws:bedrock-agentcore:us-east-1:123:runtime/custom'; + 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: overrideArn }, + blueprintConfig: { compute_type: 'agentcore', runtime_arn: runtimeArn }, }); - expect(handle.metadata.runtimeArn).toBe(overrideArn); + expect(handle.metadata.runtimeArn).toBe(runtimeArn); const invokeCall = mockSend.mock.calls[0][0]; - expect(invokeCall.input.agentRuntimeArn).toBe(overrideArn); + expect(invokeCall.input.agentRuntimeArn).toBe(runtimeArn); + }); + + test('reuses shared BedrockAgentCoreClient across instances', async () => { + const { BedrockAgentCoreClient } = require('@aws-sdk/client-bedrock-agentcore'); + // The lazy singleton may already be initialized from prior tests. + // Record the current call count, then verify no additional constructor calls happen. + const callsBefore = BedrockAgentCoreClient.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 = BedrockAgentCoreClient.mock.calls.length; + expect(callsAfter - callsBefore).toBeLessThanOrEqual(1); + expect(mockSend).toHaveBeenCalledTimes(2); }); }); describe('pollSession', () => { - test('returns running status', async () => { - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); - const result = await strategy.pollSession({ - sessionId: 'test-session', - strategyType: 'agentcore', - metadata: { runtimeArn: defaultRuntimeArn }, - }); - expect(result.status).toBe('running'); + test('throws because AgentCore polling is done at orchestrator level', async () => { + const strategy = new AgentCoreComputeStrategy(); + await expect( + strategy.pollSession({ + sessionId: 'test-session', + strategyType: 'agentcore', + metadata: { runtimeArn: defaultRuntimeArn }, + }), + ).rejects.toThrow('pollSession is not implemented for AgentCore'); }); }); describe('stopSession', () => { test('sends StopRuntimeSessionCommand', async () => { mockSend.mockResolvedValueOnce({}); - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + const strategy = new AgentCoreComputeStrategy(); await strategy.stopSession({ sessionId: 'test-session', @@ -101,9 +129,54 @@ describe('AgentCoreComputeStrategy', () => { expect(call.input.runtimeSessionId).toBe('test-session'); }); - test('does not throw when stop fails', async () => { - mockSend.mockRejectedValueOnce(new Error('Access denied')); - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + 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', + metadata: { 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', + metadata: { 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', + metadata: { 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({ @@ -115,7 +188,7 @@ describe('AgentCoreComputeStrategy', () => { }); test('skips stop when no runtimeArn in metadata', async () => { - const strategy = new AgentCoreComputeStrategy({ runtimeArn: defaultRuntimeArn }); + const strategy = new AgentCoreComputeStrategy(); await strategy.stopSession({ sessionId: 'test-session', 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..5aab773 --- /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 { resolveComputeStrategy } from '../../src/handlers/shared/compute-strategy'; +import { transitionTask, emitTaskEvent, failTask } from '../../src/handlers/shared/orchestrator'; +import { TaskStatus } from '../../src/constructs/task-status'; +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); + }); +}); From 4b004a30703da285a601b3d95778612e3a7c79ad Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Wed, 8 Apr 2026 11:46:28 -0700 Subject: [PATCH 7/8] fix: resolve ESLint errors in test files - Replace require() with ES import for BedrockAgentCoreClient mock - Fix import ordering in start-session-composition test --- .../handlers/shared/strategies/agentcore-strategy.test.ts | 7 ++++--- cdk/test/handlers/start-session-composition.test.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts index 0310f3c..c544a71 100644 --- a/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts +++ b/cdk/test/handlers/shared/strategies/agentcore-strategy.test.ts @@ -24,8 +24,10 @@ jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ 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(() => { @@ -72,10 +74,9 @@ describe('AgentCoreComputeStrategy', () => { }); test('reuses shared BedrockAgentCoreClient across instances', async () => { - const { BedrockAgentCoreClient } = require('@aws-sdk/client-bedrock-agentcore'); // The lazy singleton may already be initialized from prior tests. // Record the current call count, then verify no additional constructor calls happen. - const callsBefore = BedrockAgentCoreClient.mock.calls.length; + const callsBefore = MockedClient.mock.calls.length; mockSend.mockResolvedValue({}); const strategy1 = new AgentCoreComputeStrategy(); @@ -93,7 +94,7 @@ describe('AgentCoreComputeStrategy', () => { }); // Lazy singleton: at most one constructor call total across all strategy instances - const callsAfter = BedrockAgentCoreClient.mock.calls.length; + const callsAfter = MockedClient.mock.calls.length; expect(callsAfter - callsBefore).toBeLessThanOrEqual(1); expect(mockSend).toHaveBeenCalledTimes(2); }); diff --git a/cdk/test/handlers/start-session-composition.test.ts b/cdk/test/handlers/start-session-composition.test.ts index 5aab773..e9592d6 100644 --- a/cdk/test/handlers/start-session-composition.test.ts +++ b/cdk/test/handlers/start-session-composition.test.ts @@ -67,9 +67,9 @@ process.env.RUNTIME_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runt 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 { TaskStatus } from '../../src/constructs/task-status'; import type { BlueprintConfig } from '../../src/handlers/shared/repo-config'; beforeEach(() => { From 51882aaa367c09155a098a0f41b06a9b8e9292ac Mon Sep 17 00:00:00 2001 From: Michael Walker Date: Wed, 8 Apr 2026 15:33:42 -0700 Subject: [PATCH 8/8] feat(compute): implement ECS Fargate backend via ComputeStrategy pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire ECS Fargate as a compute backend behind the existing ComputeStrategy interface, using the existing durable Lambda orchestrator. No separate stacks or Step Functions — ECS is a strategy option alongside AgentCore. Changes: - EcsComputeStrategy: startSession (RunTask), pollSession (DescribeTasks state mapping), stopSession (StopTask with graceful error handling) - EcsAgentCluster construct: ECS Cluster (container insights), Fargate task def (2 vCPU/4GB/ARM64), security group (TCP 443 egress only), CloudWatch log group, task role (DynamoDB, SecretsManager, Bedrock) - TaskOrchestrator: optional ECS props for env vars and IAM policies (ecs:RunTask/DescribeTasks/StopTask conditioned on cluster ARN, iam:PassRole conditioned on ecs-tasks.amazonaws.com) - Orchestrator polling: ECS compute-level crash detection alongside existing DDB polling (non-fatal, wrapped in try/catch) - AgentStack: conditional ECS infrastructure (ABCA_ENABLE_ECS env var) - Full test coverage: 15 ECS strategy tests, 9 construct tests, 5 orchestrator ECS tests. All 563 tests pass. Deployed and verified: stack deploys cleanly, CDK synth passes cdk-nag, agent task running on AgentCore path unaffected. --- cdk/package.json | 1 + cdk/src/constructs/ecs-agent-cluster.ts | 150 ++++ cdk/src/constructs/task-orchestrator.ts | 60 +- cdk/src/handlers/orchestrate-task.ts | 44 +- cdk/src/handlers/shared/compute-strategy.ts | 3 +- .../shared/strategies/ecs-strategy.ts | 176 +++++ cdk/src/stacks/agent.ts | 33 + cdk/test/constructs/ecs-agent-cluster.test.ts | 198 ++++++ cdk/test/constructs/task-orchestrator.test.ts | 115 ++- .../handlers/shared/compute-strategy.test.ts | 22 +- .../shared/strategies/ecs-strategy.test.ts | 285 ++++++++ yarn.lock | 652 ++++++++++++++++++ 12 files changed, 1720 insertions(+), 19 deletions(-) create mode 100644 cdk/src/constructs/ecs-agent-cluster.ts create mode 100644 cdk/src/handlers/shared/strategies/ecs-strategy.ts create mode 100644 cdk/test/constructs/ecs-agent-cluster.test.ts create mode 100644 cdk/test/handlers/shared/strategies/ecs-strategy.test.ts diff --git a/cdk/package.json b/cdk/package.json index 85a046b..0feaf5b 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -18,6 +18,7 @@ "@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-ecs": "^3.1021.0", "@aws-sdk/client-dynamodb": "^3.1021.0", "@aws-sdk/client-lambda": "^3.1021.0", "@aws-sdk/client-secrets-manager": "^3.1021.0", diff --git a/cdk/src/constructs/ecs-agent-cluster.ts b/cdk/src/constructs/ecs-agent-cluster.ts new file mode 100644 index 0000000..b7158d5 --- /dev/null +++ b/cdk/src/constructs/ecs-agent-cluster.ts @@ -0,0 +1,150 @@ +/** + * 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; + + 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', { + logGroupName: '/ecs/abca-agent-tasks', + 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, + ...(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); + + 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-orchestrator.ts b/cdk/src/constructs/task-orchestrator.ts index 34f5944..a0ea5aa 100644 --- a/cdk/src/constructs/task-orchestrator.ts +++ b/cdk/src/constructs/task-orchestrator.ts @@ -100,6 +100,32 @@ export interface TaskOrchestratorProps { * and writes fallback episodes during finalization. */ readonly memoryId?: string; + + /** + * ARN of the ECS cluster for ECS compute strategy. + * When provided, ECS-related env vars and IAM policies are added. + */ + readonly ecsClusterArn?: string; + + /** + * ARN of the ECS task definition for ECS compute strategy. + */ + readonly ecsTaskDefinitionArn?: string; + + /** + * Comma-separated subnet IDs for ECS tasks. + */ + readonly ecsSubnets?: string; + + /** + * Security group ID for ECS tasks. + */ + readonly ecsSecurityGroup?: string; + + /** + * Container name in the ECS task definition. + */ + readonly ecsContainerName?: string; } /** @@ -152,6 +178,11 @@ export class TaskOrchestrator extends Construct { USER_PROMPT_TOKEN_BUDGET: String(props.userPromptTokenBudget), }), ...(props.memoryId && { MEMORY_ID: props.memoryId }), + ...(props.ecsClusterArn && { ECS_CLUSTER_ARN: props.ecsClusterArn }), + ...(props.ecsTaskDefinitionArn && { ECS_TASK_DEFINITION_ARN: props.ecsTaskDefinitionArn }), + ...(props.ecsSubnets && { ECS_SUBNETS: props.ecsSubnets }), + ...(props.ecsSecurityGroup && { ECS_SECURITY_GROUP: props.ecsSecurityGroup }), + ...(props.ecsContainerName && { ECS_CONTAINER_NAME: props.ecsContainerName }), }, bundling: { externalModules: ['@aws-sdk/*'], @@ -192,6 +223,33 @@ export class TaskOrchestrator extends Construct { resources: runtimeResources, })); + // ECS compute strategy permissions (only when ECS is configured) + if (props.ecsClusterArn) { + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: [ + 'ecs:RunTask', + 'ecs:DescribeTasks', + 'ecs:StopTask', + ], + resources: ['*'], + conditions: { + ArnEquals: { + 'ecs:cluster': props.ecsClusterArn, + }, + }, + })); + + this.fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['iam:PassRole'], + resources: ['*'], + conditions: { + StringEquals: { + 'iam:PassedToService': 'ecs-tasks.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( @@ -229,7 +287,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 conditioned on ecs-tasks.amazonaws.com', }, ], true); } diff --git a/cdk/src/handlers/orchestrate-task.ts b/cdk/src/handlers/orchestrate-task.ts index 0fcb757..c222bc7 100644 --- a/cdk/src/handlers/orchestrate-task.ts +++ b/cdk/src/handlers/orchestrate-task.ts @@ -119,7 +119,8 @@ const durableHandler: DurableExecutionHandler = asyn }); // Step 4: Start agent session — resolve compute strategy, invoke runtime, transition to RUNNING - await context.step('start-session', async () => { + // Returns the full SessionHandle (serializable) so ECS polling can use it in step 5. + const sessionHandle = await context.step('start-session', async () => { try { const strategy = resolveComputeStrategy(blueprintConfig); const handle = await strategy.startSession({ taskId, payload, blueprintConfig }); @@ -139,7 +140,7 @@ const durableHandler: DurableExecutionHandler = asyn strategy_type: handle.strategyType, }); - return handle.sessionId; + return handle; } catch (err) { await failTask(taskId, TaskStatus.HYDRATING, `Session start failed: ${String(err)}`, task.user_id, true); throw err; @@ -147,16 +148,41 @@ const durableHandler: DurableExecutionHandler = asyn }); // Step 5: Wait for agent to finish - // NOTE: Polls DynamoDB every 30s rather than re-invoking the AgentCore session. - // The agent writes terminal status directly to DDB. If the agent crashes without - // writing a terminal status, we detect it via the HYDRATING early-exit check - // (MAX_NON_RUNNING_POLLS ~5min); otherwise the loop runs up to MAX_POLL_ATTEMPTS - // (~8.5h). A future improvement could add AgentCore session status checks for - // faster crash detection. + // Polls DynamoDB every 30s. For ECS compute, also polls the ECS task to detect + // container crashes that don't write terminal status to DDB. For AgentCore, + // crash detection relies on the HYDRATING early-exit (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); + + // ECS compute-level crash detection: if DDB is not terminal, check ECS task status + if ( + ddbState.lastStatus && + !TERMINAL_STATUSES.includes(ddbState.lastStatus) && + blueprintConfig.compute_type === 'ecs' + ) { + try { + const strategy = resolveComputeStrategy(blueprintConfig); + const ecsStatus = await strategy.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 }; + } + } catch (err) { + logger.warn('ECS pollSession check failed (non-fatal)', { + task_id: taskId, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return ddbState; }, { initialState: { attempts: 0 }, diff --git a/cdk/src/handlers/shared/compute-strategy.ts b/cdk/src/handlers/shared/compute-strategy.ts index 5bf2093..321d92a 100644 --- a/cdk/src/handlers/shared/compute-strategy.ts +++ b/cdk/src/handlers/shared/compute-strategy.ts @@ -19,6 +19,7 @@ import type { BlueprintConfig, ComputeType } from './repo-config'; import { AgentCoreComputeStrategy } from './strategies/agentcore-strategy'; +import { EcsComputeStrategy } from './strategies/ecs-strategy'; export interface SessionHandle { readonly sessionId: string; @@ -48,7 +49,7 @@ export function resolveComputeStrategy(blueprintConfig: BlueprintConfig): Comput case 'agentcore': return new AgentCoreComputeStrategy(); case 'ecs': - throw new Error("compute_type 'ecs' is not yet implemented"); + return new EcsComputeStrategy(); default: { const _exhaustive: never = computeType; throw new Error(`Unknown compute_type: '${_exhaustive}'`); 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..96c2470 --- /dev/null +++ b/cdk/src/handlers/shared/strategies/ecs-strategy.ts @@ -0,0 +1,176 @@ +/** + * 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(','); + const { taskId, payload, blueprintConfig } = input; + + 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' }, + ]; + + 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, + }], + }, + }); + + 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: this.type, + metadata: { + clusterArn: ECS_CLUSTER_ARN, + taskArn: ecsTask.taskArn, + }, + }; + } + + async pollSession(handle: SessionHandle): Promise { + const clusterArn = handle.metadata.clusterArn as string; + const taskArn = handle.metadata.taskArn as string; + + if (!clusterArn || !taskArn) { + return { status: 'failed', error: 'Missing clusterArn or taskArn in session 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' }; + } + return { status: 'failed', error: `Exit code ${exitCode}: ${stoppedReason}` }; + } + + // PENDING, PROVISIONING, ACTIVATING, RUNNING, DEACTIVATING, DEPROVISIONING + return { status: 'running' }; + } + + async stopSession(handle: SessionHandle): Promise { + const clusterArn = handle.metadata.clusterArn as string; + const taskArn = handle.metadata.taskArn as string; + + if (!clusterArn || !taskArn) { + logger.warn('No clusterArn/taskArn in session handle, cannot stop ECS task', { + session_id: handle.sessionId, + }); + return; + } + + 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 as Error & { name?: string }).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/stacks/agent.ts b/cdk/src/stacks/agent.ts index 9500ede..6e46003 100644 --- a/cdk/src/stacks/agent.ts +++ b/cdk/src/stacks/agent.ts @@ -23,6 +23,7 @@ import * as bedrock from '@aws-cdk/aws-bedrock-alpha'; import * as agentcoremixins from '@aws-cdk/mixins-preview/aws-bedrockagentcore'; import { Stack, StackProps, RemovalPolicy, CfnOutput } from 'aws-cdk-lib'; 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 secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; @@ -34,6 +35,7 @@ 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 { RepoTable } from '../constructs/repo-table'; import { TaskApi } from '../constructs/task-api'; import { TaskDashboard } from '../constructs/task-dashboard'; @@ -248,6 +250,30 @@ export class AgentStack extends Stack { inputGuardrail.createVersion('Initial version'); + // --- ECS Fargate compute backend (conditional) --- + // Wire ECS infrastructure when any blueprint uses compute.type: 'ecs' + const hasEcsBlueprint = blueprints.some(b => b.node.findAll() + .some(() => false)); // Currently no blueprints use ECS — infrastructure is opt-in + const needsEcs = hasEcsBlueprint || process.env.ABCA_ENABLE_ECS === 'true'; + + let ecsCluster: EcsAgentCluster | undefined; + if (needsEcs) { + const agentImageAsset = new ecr_assets.DockerImageAsset(this, 'AgentImage', { + directory: runnerPath, + platform: ecr_assets.Platform.LINUX_ARM64, + }); + + ecsCluster = new EcsAgentCluster(this, 'EcsAgentCluster', { + 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, @@ -257,6 +283,13 @@ export class AgentStack extends Stack { runtimeArn: runtime.agentRuntimeArn, githubTokenSecretArn: githubTokenSecret.secretArn, memoryId: agentMemory.memory.memoryId, + ...(ecsCluster && { + ecsClusterArn: ecsCluster.cluster.clusterArn, + ecsTaskDefinitionArn: ecsCluster.taskDefinition.taskDefinitionArn, + ecsSubnets: agentVpc.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds.join(','), + ecsSecurityGroup: ecsCluster.securityGroup.securityGroupId, + ecsContainerName: ecsCluster.containerName, + }), }); // Grant the orchestrator Lambda read+write access to memory 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..151253e --- /dev/null +++ b/cdk/test/constructs/ecs-agent-cluster.test.ts @@ -0,0 +1,198 @@ +/** + * 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', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::Logs::LogGroup', { + LogGroupName: '/ecs/abca-agent-tasks', + RetentionInDays: 90, + }); + }); + + 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-orchestrator.test.ts b/cdk/test/constructs/task-orchestrator.test.ts index 7748b1d..4e9ccac 100644 --- a/cdk/test/constructs/task-orchestrator.test.ts +++ b/cdk/test/constructs/task-orchestrator.test.ts @@ -30,6 +30,11 @@ interface StackOverrides { additionalRuntimeArns?: string[]; additionalSecretArns?: string[]; memoryId?: string; + ecsClusterArn?: string; + ecsTaskDefinitionArn?: string; + ecsSubnets?: string; + ecsSecurityGroup?: string; + ecsContainerName?: string; } function createStack(overrides?: StackOverrides): { stack: Stack; template: Template } { @@ -55,7 +60,18 @@ function createStack(overrides?: StackOverrides): { stack: Stack; template: Temp }) : undefined; - const { includeRepoTable: _, additionalRuntimeArns, additionalSecretArns, memoryId, ...rest } = overrides ?? {}; + const { + includeRepoTable: _, + additionalRuntimeArns, + additionalSecretArns, + memoryId, + ecsClusterArn, + ecsTaskDefinitionArn, + ecsSubnets, + ecsSecurityGroup, + ecsContainerName, + ...rest + } = overrides ?? {}; new TaskOrchestrator(stack, 'TaskOrchestrator', { taskTable, @@ -66,6 +82,11 @@ function createStack(overrides?: StackOverrides): { stack: Stack; template: Temp ...(additionalRuntimeArns && { additionalRuntimeArns }), ...(additionalSecretArns && { additionalSecretArns }), ...(memoryId && { memoryId }), + ...(ecsClusterArn && { ecsClusterArn }), + ...(ecsTaskDefinitionArn && { ecsTaskDefinitionArn }), + ...(ecsSubnets && { ecsSubnets }), + ...(ecsSecurityGroup && { ecsSecurityGroup }), + ...(ecsContainerName && { ecsContainerName }), ...rest, }); @@ -341,4 +362,96 @@ describe('TaskOrchestrator construct', () => { MaximumRetryAttempts: 0, }); }); + + describe('ECS compute strategy', () => { + const ecsOverrides = { + ecsClusterArn: 'arn:aws:ecs:us-east-1:123456789012:cluster/agent-cluster', + ecsTaskDefinitionArn: 'arn:aws:ecs:us-east-1:123456789012:task-definition/agent:1', + ecsSubnets: 'subnet-aaa,subnet-bbb', + ecsSecurityGroup: 'sg-12345', + ecsContainerName: 'AgentContainer', + }; + + 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 conditioned on ecs-tasks.amazonaws.com', () => { + const { template } = createStack(ecsOverrides); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'iam:PassRole', + Effect: 'Allow', + Resource: '*', + 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/shared/compute-strategy.test.ts b/cdk/test/handlers/shared/compute-strategy.test.ts index 92f589c..1fac73d 100644 --- a/cdk/test/handlers/shared/compute-strategy.test.ts +++ b/cdk/test/handlers/shared/compute-strategy.test.ts @@ -23,8 +23,16 @@ jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ StopRuntimeSessionCommand: jest.fn(), })); +jest.mock('@aws-sdk/client-ecs', () => ({ + ECSClient: jest.fn(() => ({ send: jest.fn() })), + RunTaskCommand: jest.fn(), + DescribeTasksCommand: jest.fn(), + StopTaskCommand: jest.fn(), +})); + import { resolveComputeStrategy } from '../../../src/handlers/shared/compute-strategy'; import { AgentCoreComputeStrategy } from '../../../src/handlers/shared/strategies/agentcore-strategy'; +import { EcsComputeStrategy } from '../../../src/handlers/shared/strategies/ecs-strategy'; describe('resolveComputeStrategy', () => { test('returns AgentCoreComputeStrategy for compute_type agentcore', () => { @@ -36,12 +44,12 @@ describe('resolveComputeStrategy', () => { expect(strategy.type).toBe('agentcore'); }); - test("throws 'not yet implemented' for compute_type ecs", () => { - expect(() => - resolveComputeStrategy({ - compute_type: 'ecs', - runtime_arn: 'arn:test', - }), - ).toThrow("compute_type 'ecs' is not yet implemented"); + 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'); }); }); 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..145484b --- /dev/null +++ b/cdk/test/handlers/shared/strategies/ecs-strategy.test.ts @@ -0,0 +1,285 @@ +/** + * 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'); + expect(handle.metadata.clusterArn).toBe(CLUSTER_ARN); + expect(handle.metadata.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 envVars = call.input.overrides.containerOverrides[0].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' }, + ])); + }); + + 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, + metadata: { 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 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('returns failed when metadata is missing', async () => { + const strategy = new EcsComputeStrategy(); + const result = await strategy.pollSession({ + sessionId: 'test', + strategyType: 'ecs', + metadata: {}, + }); + expect(result).toEqual({ + status: 'failed', + error: 'Missing clusterArn or taskArn in session handle', + }); + }); + }); + + describe('stopSession', () => { + test('sends StopTaskCommand', async () => { + mockSend.mockResolvedValueOnce({}); + + const strategy = new EcsComputeStrategy(); + await strategy.stopSession({ + sessionId: TASK_ARN, + strategyType: 'ecs', + metadata: { 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', + metadata: { 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', + metadata: { clusterArn: CLUSTER_ARN, taskArn: TASK_ARN }, + }), + ).resolves.toBeUndefined(); + }); + + test('skips stop when metadata is missing', async () => { + const strategy = new EcsComputeStrategy(); + await strategy.stopSession({ + sessionId: 'test', + strategyType: 'ecs', + metadata: {}, + }); + + expect(mockSend).not.toHaveBeenCalled(); + }); + + 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', + metadata: { clusterArn: CLUSTER_ARN, taskArn: TASK_ARN }, + }), + ).resolves.toBeUndefined(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 67fe6df..2a8f55c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -454,6 +454,52 @@ "@smithy/util-waiter" "^4.2.14" 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" @@ -568,6 +614,25 @@ "@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/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 +644,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 +671,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 +707,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 +741,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 +773,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 +803,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 +829,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 +856,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" @@ -760,6 +943,16 @@ "@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-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 +962,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" @@ -794,6 +1007,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 +1083,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 +1138,17 @@ "@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/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 +1162,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 +1183,14 @@ "@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-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 +1209,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" @@ -933,6 +1247,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 +1269,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 +1290,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" @@ -2580,6 +2925,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 +2953,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 +2980,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" @@ -2663,6 +3047,17 @@ "@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-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 +3068,16 @@ "@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/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 +3086,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" @@ -2704,6 +3117,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 +3140,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 +3169,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 +3195,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 +3213,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 +3231,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 +3251,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 +3269,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 +3285,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 +3302,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 +3319,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 +3334,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 +3349,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 +3371,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 +3398,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 +3418,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 +3434,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 +3499,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 +3522,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 +3544,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 +3568,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 +3585,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 +3608,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 +3653,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"