From 7d227251bf7a1d08c9db098bd553d017fc8d6a0a Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 24 Apr 2026 14:35:28 -0400 Subject: [PATCH 1/4] Remove DockerImageAsset from vended cdk-stack.ts Container image building is now handled by AgentCoreHarnessEnvironment inside the L3 CDK construct via CodeBuild, matching the runtime pattern. The vended stack no longer needs DockerImageAsset, the toPascalId helper, or the per-harness build loop. Update snapshot to match. --- .../assets.snapshot.test.ts.snap | 46 ++----------------- src/assets/cdk/lib/cdk-stack.ts | 46 ++----------------- 2 files changed, 10 insertions(+), 82 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 5a3b728b..9bcefd05 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -301,10 +301,6 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou AgentCoreMcp, type AgentCoreProjectSpec, type AgentCoreMcpSpec, - ContainerSourceAssetFromPath, - AgentEcrRepository, - ContainerBuildProject, - ContainerImageBuilder, } from '@aws/agentcore-cdk'; import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; @@ -336,6 +332,10 @@ export interface AgentCoreStackProps extends StackProps { credentials?: Record; /** * Harness role configurations. Each entry creates an IAM execution role for a harness. + * + * When \`hasDockerfile\` is true and \`harnessDir\` is provided (without an explicit + * \`containerUri\`), the L3 construct builds and pushes a container image via CodeBuild + * and emits its URI as a stack output for the post-CDK harness deployer. */ harnesses?: HarnessConfig[]; } @@ -355,46 +355,10 @@ export class AgentCoreStack extends Stack { const { spec, mcpSpec, credentials, harnesses } = props; - // Build container images for harnesses that specify a dockerfile (no containerUri). - // Produces CDK outputs consumed by the imperative harness deployer. - const harnessesForCdk = harnesses ? [...harnesses] : []; - if (harnesses) { - for (let i = 0; i < harnesses.length; i++) { - const h = harnesses[i]!; - if (h.hasDockerfile && !h.containerUri && h.harnessDir) { - const pascalName = h.name.replace(/(^|_)([a-z])/g, (_: string, __: string, c: string) => c.toUpperCase()); - const sourceAsset = new ContainerSourceAssetFromPath(this, \`Harness\${pascalName}SourceAsset\`, { - sourcePath: h.harnessDir, - }); - const ecrRepo = new AgentEcrRepository(this, \`Harness\${pascalName}EcrRepo\`, { - projectName: spec.name, - agentName: \`harness-\${h.name}\`, - }); - const buildProject = ContainerBuildProject.getOrCreate(this); - buildProject.grantPushTo(ecrRepo.repository); - sourceAsset.asset.grantRead(buildProject.role); - - const builder = new ContainerImageBuilder(this, \`Harness\${pascalName}ContainerBuild\`, { - buildProject, - sourceAsset, - repository: ecrRepo, - dockerfile: h.dockerfileName ?? 'Dockerfile', - }); - - new CfnOutput(this, \`Harness\${pascalName}ContainerUriOutput\`, { - value: builder.containerUri, - }); - - // Pass the built containerUri to the harness role construct so it gets ECR pull permissions - harnessesForCdk[i] = { ...h, containerUri: builder.containerUri }; - } - } - } - // Create AgentCoreApplication with all agents and harness roles this.application = new AgentCoreApplication(this, 'Application', { spec, - harnesses: harnessesForCdk.length > 0 ? harnessesForCdk : undefined, + harnesses: harnesses?.length ? harnesses : undefined, }); // Create AgentCoreMcp if there are gateways configured diff --git a/src/assets/cdk/lib/cdk-stack.ts b/src/assets/cdk/lib/cdk-stack.ts index 40c6ec16..898ecd84 100644 --- a/src/assets/cdk/lib/cdk-stack.ts +++ b/src/assets/cdk/lib/cdk-stack.ts @@ -3,10 +3,6 @@ import { AgentCoreMcp, type AgentCoreProjectSpec, type AgentCoreMcpSpec, - ContainerSourceAssetFromPath, - AgentEcrRepository, - ContainerBuildProject, - ContainerImageBuilder, } from '@aws/agentcore-cdk'; import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; @@ -38,6 +34,10 @@ export interface AgentCoreStackProps extends StackProps { credentials?: Record; /** * Harness role configurations. Each entry creates an IAM execution role for a harness. + * + * When `hasDockerfile` is true and `harnessDir` is provided (without an explicit + * `containerUri`), the L3 construct builds and pushes a container image via CodeBuild + * and emits its URI as a stack output for the post-CDK harness deployer. */ harnesses?: HarnessConfig[]; } @@ -57,46 +57,10 @@ export class AgentCoreStack extends Stack { const { spec, mcpSpec, credentials, harnesses } = props; - // Build container images for harnesses that specify a dockerfile (no containerUri). - // Produces CDK outputs consumed by the imperative harness deployer. - const harnessesForCdk = harnesses ? [...harnesses] : []; - if (harnesses) { - for (let i = 0; i < harnesses.length; i++) { - const h = harnesses[i]!; - if (h.hasDockerfile && !h.containerUri && h.harnessDir) { - const pascalName = h.name.replace(/(^|_)([a-z])/g, (_: string, __: string, c: string) => c.toUpperCase()); - const sourceAsset = new ContainerSourceAssetFromPath(this, `Harness${pascalName}SourceAsset`, { - sourcePath: h.harnessDir, - }); - const ecrRepo = new AgentEcrRepository(this, `Harness${pascalName}EcrRepo`, { - projectName: spec.name, - agentName: `harness-${h.name}`, - }); - const buildProject = ContainerBuildProject.getOrCreate(this); - buildProject.grantPushTo(ecrRepo.repository); - sourceAsset.asset.grantRead(buildProject.role); - - const builder = new ContainerImageBuilder(this, `Harness${pascalName}ContainerBuild`, { - buildProject, - sourceAsset, - repository: ecrRepo, - dockerfile: h.dockerfileName ?? 'Dockerfile', - }); - - new CfnOutput(this, `Harness${pascalName}ContainerUriOutput`, { - value: builder.containerUri, - }); - - // Pass the built containerUri to the harness role construct so it gets ECR pull permissions - harnessesForCdk[i] = { ...h, containerUri: builder.containerUri }; - } - } - } - // Create AgentCoreApplication with all agents and harness roles this.application = new AgentCoreApplication(this, 'Application', { spec, - harnesses: harnessesForCdk.length > 0 ? harnessesForCdk : undefined, + harnesses: harnesses?.length ? harnesses : undefined, }); // Create AgentCoreMcp if there are gateways configured From 5e570f14b6ca74e2375672e16c743d32e35edb08 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 24 Apr 2026 15:06:10 -0400 Subject: [PATCH 2/4] Support both old and new CDK output key patterns for harness outputs The new AgentCoreHarnessEnvironment construct nests AgentCoreHarnessRole under a 'Role' child, which changes the CDK output key from ApplicationHarnessRoleArnOutput to ApplicationHarnessRole RoleArnOutput. Similarly, the container URI output moves from a stack- level HarnessContainerUri key to ApplicationHarnessImageUri. Update both resolvers to try the new pattern first, falling back to the old pattern for backward compatibility with existing stacks that haven't upgraded their CDK constructs. --- .../imperative/deployers/harness-deployer.ts | 19 +++++++++++++------ .../imperative/deployers/harness-mapper.ts | 13 ++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts index ccb383b7..bacb97fb 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-deployer.ts @@ -102,7 +102,11 @@ export class HarnessDeployer implements ImperativeDeployer): string | undefined { if (!cdkOutputs) return undefined; const pascalName = toPascalId(harnessName); - const prefix = `ApplicationHarness${pascalName}RoleArn`; + const prefixes = [`ApplicationHarness${pascalName}RoleRoleArn`, `ApplicationHarness${pascalName}RoleArn`]; for (const [key, value] of Object.entries(cdkOutputs)) { - if (key.startsWith(prefix)) { + if (prefixes.some(p => key.startsWith(p))) { return value; } } diff --git a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts index d91623e2..e1321914 100644 --- a/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts +++ b/src/cli/operations/deploy/imperative/deployers/harness-mapper.ts @@ -104,7 +104,7 @@ export async function mapHarnessSpecToCreateOptions(options: MapHarnessOptions): if (!builtUri) { throw new Error( `Harness "${harnessSpec.name}" specifies "dockerfile" but no container URI was found in CDK outputs. ` + - `Expected a CDK output key starting with "Harness${toPascalId(harnessSpec.name)}ContainerUri".` + `Expected a CDK output key starting with "ApplicationHarness${toPascalId(harnessSpec.name)}ImageUri" or "Harness${toPascalId(harnessSpec.name)}ContainerUri".` ); } result.environmentArtifact = mapEnvironmentArtifact(builtUri); @@ -332,14 +332,21 @@ function mapTruncation(truncation: NonNullable): Harn // Container URI Resolution (from CDK outputs for dockerfile-based harnesses) // ============================================================================ +/** + * Supports two construct tree layouts: + * Old (CfnOutput on stack root): + * Harness{PascalName}ContainerUri... + * New (CfnOutput inside AgentCoreHarnessEnvironment): + * ApplicationHarness{PascalName}ImageUriOutput... + */ function resolveContainerUriFromOutputs(harnessName: string, cdkOutputs?: Record): string | undefined { if (!cdkOutputs) return undefined; const pascalName = toPascalId(harnessName); - const prefix = `Harness${pascalName}ContainerUri`; + const prefixes = [`ApplicationHarness${pascalName}ImageUri`, `Harness${pascalName}ContainerUri`]; for (const [key, value] of Object.entries(cdkOutputs)) { - if (key.startsWith(prefix)) { + if (prefixes.some(p => key.startsWith(p))) { return value; } } From b43230e80dfe7dc29bcb39e83da1fc644a2a7ec8 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 24 Apr 2026 15:30:05 -0400 Subject: [PATCH 3/4] Align vended CDK field names with HarnessRoleConfig interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preview branch renamed dockerfile→dockerfileName and codeLocation→harnessDir, but the CDK construct's HarnessRoleConfig still uses dockerfile and codeLocation. Without matching field names, AgentCoreHarnessEnvironment never sees the dockerfile and skips the CodeBuild container build entirely. --- .../__snapshots__/assets.snapshot.test.ts.snap | 14 +++++++------- src/assets/cdk/bin/cdk.ts | 8 ++++---- src/assets/cdk/lib/cdk-stack.ts | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 9bcefd05..9eb78699 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -110,8 +110,8 @@ async function main() { memoryName?: string; containerUri?: string; hasDockerfile?: boolean; - dockerfileName?: string; - harnessDir?: string; + dockerfile?: string; + codeLocation?: string; tools?: { type: string; name: string }[]; apiKeyArn?: string; }[] = []; @@ -126,8 +126,8 @@ async function main() { memoryName: harnessSpec.memory?.name, containerUri: harnessSpec.containerUri, hasDockerfile: !!harnessSpec.dockerfile, - dockerfileName: harnessSpec.dockerfile, - harnessDir, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, tools: harnessSpec.tools, apiKeyArn: harnessSpec.model?.apiKeyArn, }); @@ -311,8 +311,8 @@ export interface HarnessConfig { memoryName?: string; containerUri?: string; hasDockerfile?: boolean; - dockerfileName?: string; - harnessDir?: string; + dockerfile?: string; + codeLocation?: string; tools?: { type: string; name: string }[]; apiKeyArn?: string; } @@ -333,7 +333,7 @@ export interface AgentCoreStackProps extends StackProps { /** * Harness role configurations. Each entry creates an IAM execution role for a harness. * - * When \`hasDockerfile\` is true and \`harnessDir\` is provided (without an explicit + * When \`hasDockerfile\` is true and \`codeLocation\` is provided (without an explicit * \`containerUri\`), the L3 construct builds and pushes a container image via CodeBuild * and emits its URI as a stack output for the post-CDK harness deployer. */ diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 57b29238..1c010e19 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -65,8 +65,8 @@ async function main() { memoryName?: string; containerUri?: string; hasDockerfile?: boolean; - dockerfileName?: string; - harnessDir?: string; + dockerfile?: string; + codeLocation?: string; tools?: { type: string; name: string }[]; apiKeyArn?: string; }[] = []; @@ -81,8 +81,8 @@ async function main() { memoryName: harnessSpec.memory?.name, containerUri: harnessSpec.containerUri, hasDockerfile: !!harnessSpec.dockerfile, - dockerfileName: harnessSpec.dockerfile, - harnessDir, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, tools: harnessSpec.tools, apiKeyArn: harnessSpec.model?.apiKeyArn, }); diff --git a/src/assets/cdk/lib/cdk-stack.ts b/src/assets/cdk/lib/cdk-stack.ts index 898ecd84..a89efc85 100644 --- a/src/assets/cdk/lib/cdk-stack.ts +++ b/src/assets/cdk/lib/cdk-stack.ts @@ -13,8 +13,8 @@ export interface HarnessConfig { memoryName?: string; containerUri?: string; hasDockerfile?: boolean; - dockerfileName?: string; - harnessDir?: string; + dockerfile?: string; + codeLocation?: string; tools?: { type: string; name: string }[]; apiKeyArn?: string; } @@ -35,7 +35,7 @@ export interface AgentCoreStackProps extends StackProps { /** * Harness role configurations. Each entry creates an IAM execution role for a harness. * - * When `hasDockerfile` is true and `harnessDir` is provided (without an explicit + * When `hasDockerfile` is true and `codeLocation` is provided (without an explicit * `containerUri`), the L3 construct builds and pushes a container image via CodeBuild * and emits its URI as a stack output for the post-CDK harness deployer. */ From e6dc7d0fffa48fc9575b4573ee73d060abec8e8c Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Fri, 24 Apr 2026 15:50:10 -0400 Subject: [PATCH 4/4] Add tests for new AgentCoreHarnessEnvironment CDK output key patterns --- .../__tests__/harness-deployer.test.ts | 41 +++++++++++++++++++ .../__tests__/harness-mapper.test.ts | 16 ++++++++ 2 files changed, 57 insertions(+) diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts index 95af235e..c64558c8 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts @@ -209,6 +209,47 @@ describe('HarnessDeployer', () => { expect(result.notes).toContain('Created harness "my_harness"'); }); + it('resolves role ARN from new AgentCoreHarnessEnvironment output key', async () => { + const createOptions = { + region: REGION, + harnessName: 'my_harness', + executionRoleArn: 'arn:aws:iam::123456789012:role/HarnessRole', + model: { bedrockModelConfig: { modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' } }, + }; + + mockedReadFile.mockResolvedValueOnce(HARNESS_SPEC_JSON); + mockedMapHarness.mockResolvedValueOnce(createOptions); + mockedCreateHarness.mockResolvedValueOnce({ + harness: { + harnessId: 'h-new', + harnessName: 'my_harness', + arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/h-new', + status: 'READY', + executionRoleArn: 'arn:aws:iam::123456789012:role/HarnessRole', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + }); + + const ctx = createContext({ + harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }], + cdkOutputs: { + ApplicationHarnessMyHarnessRoleRoleArnOutput123: 'arn:aws:iam::123456789012:role/HarnessRole', + }, + }); + + const result = await deployer.deploy(ctx); + + expect(result.success).toBe(true); + expect(result.state!.my_harness).toEqual( + expect.objectContaining({ + harnessId: 'h-new', + roleArn: 'arn:aws:iam::123456789012:role/HarnessRole', + }) + ); + expect(mockedCreateHarness).toHaveBeenCalledWith(createOptions); + }); + it('updates a harness when already deployed', async () => { const createOptions = { region: REGION, diff --git a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts index cc16da9b..a87c598f 100644 --- a/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts +++ b/src/cli/operations/deploy/imperative/deployers/__tests__/harness-mapper.test.ts @@ -316,6 +316,22 @@ describe('mapHarnessSpecToCreateOptions', () => { }); }); + it('resolves containerUri from new AgentCoreHarnessEnvironment output key', async () => { + const spec = minimalSpec({ dockerfile: 'Dockerfile' }); + const cdkOutputs = { + ApplicationHarnessTestHarnessImageUriOutputABC123: + '123456789012.dkr.ecr.us-east-1.amazonaws.com/harness-test:new', + }; + + const result = await mapHarnessSpecToCreateOptions({ ...BASE_OPTIONS, harnessSpec: spec, cdkOutputs }); + + expect(result.environmentArtifact).toEqual({ + containerConfiguration: { + containerUri: '123456789012.dkr.ecr.us-east-1.amazonaws.com/harness-test:new', + }, + }); + }); + it('throws when dockerfile is set but no container URI found in CDK outputs', async () => { const spec = minimalSpec({ dockerfile: 'Dockerfile' });