Skip to content

Commit c13303e

Browse files
authored
Merge pull request #133 from awslabs/refactor/cdk-l2-constructs
refactor(infra-cdk): migrate to L2 constructs, drop NestedStacks; fix Cedar policy + vite 8 build
2 parents b85c965 + a7b2a0c commit c13303e

11 files changed

Lines changed: 8233 additions & 10167 deletions

frontend/package-lock.json

Lines changed: 6112 additions & 7829 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"tailwindcss": "^4",
5757
"tw-animate-css": "^1.2.9",
5858
"typescript": "^5",
59-
"vite": "^8.0.16",
59+
"vite": "^8.1.0",
6060
"vitest": "^4.1.0"
6161
}
6262
}

frontend/vite.config.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ export default defineConfig({
2020
sourcemap: true,
2121
rollupOptions: {
2222
output: {
23-
manualChunks: {
24-
"react-vendor": ["react", "react-dom", "react-router-dom"],
25-
"ui-vendor": [
26-
"@radix-ui/react-dialog",
27-
"@radix-ui/react-select",
28-
"@radix-ui/react-alert-dialog",
29-
"@radix-ui/react-progress",
30-
],
31-
"auth-vendor": ["react-oidc-context", "aws-amplify"],
23+
// Vite 8 uses the rolldown bundler, which requires manualChunks to be a
24+
// function — the object form is no longer supported and fails at build.
25+
manualChunks(id: string) {
26+
if (id.includes("node_modules")) {
27+
if (/[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/.test(id))
28+
return "react-vendor"
29+
if (id.includes("@radix-ui") || id.includes("radix-ui")) return "ui-vendor"
30+
if (id.includes("react-oidc-context") || id.includes("aws-amplify"))
31+
return "auth-vendor"
32+
}
3233
},
3334
},
3435
},

infra-cdk/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ stack_name_base: FAST-stack
55
admin_user_email: # Example: admin@example.com
66

77
backend:
8+
agent_name: FASTAgent # Name for the agent runtime (a-z, A-Z, 0-9, _)
89
pattern: strands-single-agent # See patterns/ for available options
910
# Pattern prefix determines the frontend parser:
1011
# agui-* → AG-UI parser (e.g. agui-strands-agent)

infra-cdk/lib/amplify-hosting-stack.ts renamed to infra-cdk/lib/amplify-hosting-construct.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@ import * as iam from "aws-cdk-lib/aws-iam"
55
import { Construct } from "constructs"
66
import { AppConfig } from "./utils/config-manager"
77

8-
export interface AmplifyStackProps extends cdk.NestedStackProps {
8+
export interface AmplifyConstructProps {
99
config: AppConfig
1010
}
1111

12-
export class AmplifyHostingStack extends cdk.NestedStack {
12+
export class AmplifyHostingConstruct extends Construct {
1313
public readonly amplifyApp: amplify.App
1414
public readonly amplifyUrl: string
1515
public readonly stagingBucket: s3.Bucket
1616

17-
constructor(scope: Construct, id: string, props: AmplifyStackProps) {
18-
const description = "Fullstack AgentCore Solution Template - Amplify Hosting Stack"
19-
super(scope, id, { ...props, description })
17+
constructor(scope: Construct, id: string, props: AmplifyConstructProps) {
18+
super(scope, id)
2019

2120
// Create access logs bucket for staging bucket
2221
const accessLogsBucket = new s3.Bucket(this, "StagingBucketAccessLogs", {
Lines changed: 59 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import * as dynamodb from "aws-cdk-lib/aws-dynamodb"
88
import * as apigateway from "aws-cdk-lib/aws-apigateway"
99
import * as logs from "aws-cdk-lib/aws-logs"
1010
import * as s3 from "aws-cdk-lib/aws-s3"
11-
import * as agentcore from "@aws-cdk/aws-bedrock-agentcore-alpha"
12-
import * as bedrockagentcore from "aws-cdk-lib/aws-bedrockagentcore"
11+
import * as agentcore from "aws-cdk-lib/aws-bedrockagentcore"
1312
import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"
1413
import * as lambda from "aws-cdk-lib/aws-lambda"
1514
import * as ecr_assets from "aws-cdk-lib/aws-ecr-assets"
@@ -20,30 +19,35 @@ import { AgentCoreRole } from "./utils/agentcore-role"
2019
import * as path from "path"
2120
import * as fs from "fs"
2221

23-
export interface BackendStackProps extends cdk.NestedStackProps {
22+
export interface BackendConstructProps {
2423
config: AppConfig
2524
userPoolId: string
2625
userPoolClientId: string
2726
userPoolDomain: cognito.UserPoolDomain
2827
frontendUrl: string
2928
}
3029

31-
export class BackendStack extends cdk.NestedStack {
30+
export class BackendConstruct extends Construct {
3231
public readonly userPoolId: string
3332
public readonly userPoolClientId: string
3433
public readonly userPoolDomain: cognito.UserPoolDomain
3534
public feedbackApiUrl: string
3635
public runtimeArn: string
3736
public memoryArn: string
38-
private agentName: cdk.CfnParameter
37+
private readonly region: string
38+
private readonly account: string
3939
private userPool: cognito.IUserPool
4040
private machineClient: cognito.UserPoolClient
4141
private machineClientSecret: secretsmanager.Secret
4242
private runtimeCredentialProvider: cdk.CustomResource
4343
private agentRuntime: agentcore.Runtime
4444

45-
constructor(scope: Construct, id: string, props: BackendStackProps) {
46-
super(scope, id, props)
45+
constructor(scope: Construct, id: string, props: BackendConstructProps) {
46+
super(scope, id)
47+
48+
const stack = cdk.Stack.of(this)
49+
this.region = stack.region
50+
this.account = stack.account
4751

4852
// Store the Cognito values
4953
this.userPoolId = props.userPoolId
@@ -99,13 +103,6 @@ export class BackendStack extends cdk.NestedStack {
99103
private createAgentCoreRuntime(config: AppConfig): void {
100104
const pattern = config.backend?.pattern || "strands-single-agent"
101105

102-
// Parameters
103-
this.agentName = new cdk.CfnParameter(this, "AgentName", {
104-
type: "String",
105-
default: "FASTAgent",
106-
description: "Name for the agent runtime",
107-
})
108-
109106
const stack = cdk.Stack.of(this)
110107
const deploymentType = config.backend.deployment_type
111108

@@ -268,31 +265,26 @@ export class BackendStack extends cdk.NestedStack {
268265

269266
// Create memory resource with short-term memory (conversation history) as default
270267
// To enable long-term strategies (summaries, preferences, facts), see docs/MEMORY_INTEGRATION.md
271-
const memory = new cdk.CfnResource(this, "AgentMemory", {
272-
type: "AWS::BedrockAgentCore::Memory",
273-
properties: {
274-
Name: cdk.Names.uniqueResourceName(this, { maxLength: 48 }),
275-
EventExpiryDuration: 30,
276-
Description: `Short-term memory for ${config.stack_name_base} agent`,
277-
MemoryStrategies: [
278-
{
279-
// Extracts and stores factual information shared by the user across sessions.
280-
// Stored under /facts/{actorId} — retrieved on each turn to personalise responses.
281-
SemanticMemoryStrategy: {
282-
Name: "FactExtractor",
283-
Namespaces: ["/facts/{actorId}"],
284-
},
285-
},
286-
],
287-
MemoryExecutionRoleArn: agentRole.roleArn,
288-
Tags: {
289-
Name: `${config.stack_name_base}_Memory`,
290-
ManagedBy: "CDK",
291-
},
268+
const memory = new agentcore.Memory(this, "AgentMemory", {
269+
memoryName: cdk.Names.uniqueResourceName(this, { maxLength: 48 }),
270+
expirationDuration: cdk.Duration.days(30),
271+
description: `Short-term memory for ${config.stack_name_base} agent`,
272+
memoryStrategies: [
273+
// Extracts and stores factual information shared by the user across sessions.
274+
// Stored under /facts/{actorId} — retrieved on each turn to personalise responses.
275+
agentcore.MemoryStrategy.usingSemantic({
276+
strategyName: "FactExtractor",
277+
namespaces: ["/facts/{actorId}"],
278+
}),
279+
],
280+
executionRole: agentRole,
281+
tags: {
282+
Name: `${config.stack_name_base}_Memory`,
283+
ManagedBy: "CDK",
292284
},
293285
})
294-
const memoryId = memory.getAtt("MemoryId").toString()
295-
const memoryArn = memory.getAtt("MemoryArn").toString()
286+
const memoryId = memory.memoryId
287+
const memoryArn = memory.memoryArn
296288

297289
// Store the memory ARN for access from main stack
298290
this.memoryArn = memoryArn
@@ -401,7 +393,7 @@ export class BackendStack extends cdk.NestedStack {
401393
// from RequestContext.request_headers, which is needed to securely extract the
402394
// user ID from the validated JWT token (sub claim) instead of trusting the payload body.
403395
this.agentRuntime = new agentcore.Runtime(this, "Runtime", {
404-
runtimeName: `${config.stack_name_base.replace(/-/g, "_")}_${this.agentName.valueAsString}`,
396+
runtimeName: `${config.stack_name_base.replace(/-/g, "_")}_${config.backend.agent_name}`,
405397
agentRuntimeArtifact: agentRuntimeArtifact,
406398
executionRole: agentRole,
407399
networkConfiguration: networkConfiguration,
@@ -739,7 +731,6 @@ export class BackendStack extends cdk.NestedStack {
739731

740732
// Load tool specification from JSON file
741733
const toolSpecPath = path.join(__dirname, "../../gateway/tools/sample_tool/tool_spec.json") // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
742-
const apiSpec = JSON.parse(require("fs").readFileSync(toolSpecPath, "utf8"))
743734

744735
// Cognito OAuth2 configuration for gateway
745736
const cognitoIssuer = `https://cognito-idp.${this.region}.amazonaws.com/${this.userPool.userPoolId}`
@@ -837,54 +828,32 @@ export class BackendStack extends cdk.NestedStack {
837828
// Store for use in createAgentCoreRuntime()
838829
this.runtimeCredentialProvider = runtimeCredentialProvider
839830

840-
// Create Gateway using L1 construct (CfnGateway)
841-
// This replaces the Custom Resource approach with native CloudFormation support
842-
const gateway = new bedrockagentcore.CfnGateway(this, "AgentCoreGateway", {
843-
name: `${config.stack_name_base}-gateway`,
844-
roleArn: gatewayRole.roleArn,
845-
protocolType: "MCP",
846-
protocolConfiguration: {
847-
mcp: {
848-
supportedVersions: ["2025-03-26"],
849-
// Optional: Enable semantic search for tools
850-
// searchType: "SEMANTIC",
851-
},
852-
},
853-
authorizerType: "CUSTOM_JWT",
854-
authorizerConfiguration: {
855-
customJwtAuthorizer: {
856-
allowedClients: [this.machineClient.userPoolClientId],
857-
discoveryUrl: cognitoDiscoveryUrl,
858-
},
859-
},
831+
// Create Gateway using L2 construct
832+
const gateway = new agentcore.Gateway(this, "AgentCoreGateway", {
833+
gatewayName: `${config.stack_name_base}-gateway`,
834+
role: gatewayRole,
835+
protocolConfiguration: new agentcore.McpProtocolConfiguration({
836+
supportedVersions: [agentcore.MCPProtocolVersion.MCP_2025_03_26],
837+
}),
838+
authorizerConfiguration: agentcore.GatewayAuthorizer.usingCustomJwt({
839+
discoveryUrl: cognitoDiscoveryUrl,
840+
allowedClients: [this.machineClient.userPoolClientId],
841+
}),
860842
description: "AgentCore Gateway with MCP protocol and JWT authentication",
861843
})
862844

863-
// Create Gateway Target using L1 construct (CfnGatewayTarget)
864-
const gatewayTarget = new bedrockagentcore.CfnGatewayTarget(this, "GatewayTarget", {
865-
gatewayIdentifier: gateway.attrGatewayIdentifier,
866-
name: "sample-tool-target",
845+
// Create Gateway Target using L2 addLambdaTarget().
846+
// This grants the gateway role invoke permission and adds the resource-based
847+
// Lambda permission the CreateGatewayTarget dry-run validation requires.
848+
const gatewayTarget = gateway.addLambdaTarget("GatewayTarget", {
849+
gatewayTargetName: "sample-tool-target",
867850
description: "Sample tool Lambda target",
868-
targetConfiguration: {
869-
mcp: {
870-
lambda: {
871-
lambdaArn: toolLambda.functionArn,
872-
toolSchema: {
873-
inlinePayload: apiSpec,
874-
},
875-
},
876-
},
877-
},
878-
credentialProviderConfigurations: [
879-
{
880-
credentialProviderType: "GATEWAY_IAM_ROLE",
881-
},
882-
],
851+
lambdaFunction: toolLambda,
852+
toolSchema: agentcore.ToolSchema.fromLocalAsset(toolSpecPath),
853+
// credentialProviderConfigurations defaults to [GatewayCredentialProvider.iamRole()]
883854
})
884855

885856
// Ensure proper creation order
886-
gatewayTarget.addDependency(gateway)
887-
gateway.node.addDependency(toolLambda)
888857
gateway.node.addDependency(this.machineClient)
889858
gateway.node.addDependency(gatewayRole)
890859

@@ -965,15 +934,17 @@ export class BackendStack extends cdk.NestedStack {
965934
// Grant Lambda permissions to update the Gateway (attach/detach policy engine)
966935
// and read Gateway configuration for the update_gateway call.
967936
// iam:PassRole is required because update_gateway re-associates the Gateway's IAM role.
937+
// InvokeGateway is required for CreatePolicy/UpdatePolicy: policy validation calls.
968938
cedarPolicyLambda.addToRolePolicy(
969939
new iam.PolicyStatement({
970940
actions: [
971941
"bedrock-agentcore:UpdateGateway",
972942
"bedrock-agentcore:GetGateway",
943+
"bedrock-agentcore:InvokeGateway",
973944
"bedrock-agentcore:ManageResourceScopedPolicy",
974945
"bedrock-agentcore:ListGatewayTargets",
975946
],
976-
resources: [gateway.attrGatewayArn],
947+
resources: [gateway.gatewayArn],
977948
})
978949
)
979950

@@ -1000,12 +971,12 @@ export class BackendStack extends cdk.NestedStack {
1000971
.filter((line: string) => !line.trimStart().startsWith("//"))
1001972
.join("\n")
1002973
.trim()
1003-
.replaceAll("{{GATEWAY_ARN}}", gateway.attrGatewayArn)
974+
.replaceAll("{{GATEWAY_ARN}}", gateway.gatewayArn)
1004975

1005976
const cedarPolicy = new cdk.CustomResource(this, "GatewayPolicy", {
1006977
serviceToken: cedarPolicyProvider.serviceToken,
1007978
properties: {
1008-
GatewayIdentifier: gateway.attrGatewayIdentifier,
979+
GatewayIdentifier: gateway.gatewayId,
1009980
PolicyDocument: policyDocument,
1010981
// Policy name format: {PolicyEngineName}_cp_{timestamp}
1011982
// The AgentCore API enforces a 48-character limit on policy names.
@@ -1020,28 +991,28 @@ export class BackendStack extends cdk.NestedStack {
1020991
// Store AgentCore Gateway URL in SSM for AgentCore Runtime access
1021992
new ssm.StringParameter(this, "GatewayUrlParam", {
1022993
parameterName: `/${config.stack_name_base}/gateway_url`,
1023-
stringValue: gateway.attrGatewayUrl,
994+
stringValue: gateway.gatewayUrl!,
1024995
description: "AgentCore Gateway URL",
1025996
})
1026997

1027998
// Output gateway information
1028999
new cdk.CfnOutput(this, "GatewayId", {
1029-
value: gateway.attrGatewayIdentifier,
1000+
value: gateway.gatewayId,
10301001
description: "AgentCore Gateway ID",
10311002
})
10321003

10331004
new cdk.CfnOutput(this, "GatewayUrl", {
1034-
value: gateway.attrGatewayUrl,
1005+
value: gateway.gatewayUrl!,
10351006
description: "AgentCore Gateway URL",
10361007
})
10371008

10381009
new cdk.CfnOutput(this, "GatewayArn", {
1039-
value: gateway.attrGatewayArn,
1010+
value: gateway.gatewayArn,
10401011
description: "AgentCore Gateway ARN",
10411012
})
10421013

10431014
new cdk.CfnOutput(this, "GatewayTargetId", {
1044-
value: gatewayTarget.ref,
1015+
value: gatewayTarget.targetId,
10451016
description: "AgentCore Gateway Target ID",
10461017
})
10471018

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ import * as path from "path"
77
import { Construct } from "constructs"
88
import { AppConfig } from "./utils/config-manager"
99

10-
export interface CognitoStackProps extends cdk.NestedStackProps {
10+
export interface CognitoConstructProps {
1111
config: AppConfig
1212
callbackUrls?: string[]
1313
}
1414

15-
export class CognitoStack extends cdk.NestedStack {
15+
export class CognitoConstruct extends Construct {
1616
public userPoolId: string
1717
public userPoolClientId: string
1818
public userPoolDomain: cognito.UserPoolDomain
1919

20-
constructor(scope: Construct, id: string, props: CognitoStackProps) {
21-
super(scope, id, props)
20+
constructor(scope: Construct, id: string, props: CognitoConstructProps) {
21+
super(scope, id)
2222

2323
this.createCognitoUserPool(props.config, props.callbackUrls)
2424
}

0 commit comments

Comments
 (0)