Adding required Lib folder for blueprints. (#797)
* Fix wording typo in notebook about user consent flow
cosmetic update
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
* Add pyyaml to requirements.txt
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
* Add HardikThakkar94 to CONTRIBUTORS.md
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
* Updates to fix the Streamlit app access when running in sagemaker
Modified
- Requirements.txt (added dependencies)
- chatbot_app_cognito.py (added get_streamlit_url, for sagemaker access)
- runtime_with_strands_and_egress_3lo.ipynb (streamlit piece for access url, cosmetic updates)
* Fixing Ruff errors reported by python-lint
* removing Ruff errors from python-lint
* passing 3.7 as the model for workshop
* Docs: add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook
* Revert "Docs: add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook"
This reverts commit 5dded4c38a.
* Add prerequisites (OpenAI or Azure OpenAI) cell to Outbound Auth notebook
* cosmetic fix
* Updating OpenAI URL
* Added instructions on the OAuth flow session binding and Streamlit functionality
* All imports are now properly organized at the top of the file, following Python best practices (PEP 8). The linting errors should now be resolved:
- ✅ runtime.py:18:1: E402 - Fixed
- ✅ runtime.py:19:1: E402 - Fixed
- ✅ runtime.py:19:20: F811 - Fixed
- ✅ runtime.py:25:1: E402 - Fixed
* formatting fixed
* Update Identity Outbound tutorial notebooks with corrections and improvements:
1. 05-Outbound_Auth_3lo notebook: Fixed credential provider name typo
2. 06-Outbound_Auth_Github notebook: Multiple improvements including:
- Updated description text for GitHub-specific use case
- Reorganized imports (moved to top of cell)
- Added boto session and region setup
- Reordered OAuth flow description
- Restructured notebook sections (removed redundant policy section, added clearer status check and invoke sections)
- Fixed credential provider name reference
* Fixed Identity Sections based on SageMaker (Workshop) to handle oauth2_callback_server and other cosmetic updates.
* Remove unused import and added permissions for 1st time model access for workshops
* formatting fixed.
* parameterize provider, update github image.
* added import boto3 and updated image for GitHub Session Binding
* Update Model and Remove Global Var
* Travel and Shopping concierge agents blueprints
* add missing contributors for the blueprint
* fix python-lint errors
* CodeQL fixes and config
* fix python-lint unused imports
* fix python-lint
* fix linter and cql issues
* run linter
* update codeql suppressions
* suppress codeql
* Revert accidental changes to 01-tutorials and 03-integrations
Remove files accidentally added to 01-tutorials and 03-integrations in previous commits.
These changes were not intended to be part of the blueprint additions.
Reverted files:
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/.dockerignore
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/Dockerfile
- 01-tutorials/03-AgentCore-identity/06-Outbound_Auth_Github/github_agent.py
- 03-integrations/IDP-examples/EntraID/.agentcore.json
- 03-integrations/IDP-examples/EntraID/.dockerignore
- 03-integrations/IDP-examples/EntraID/Dockerfile
- 03-integrations/IDP-examples/EntraID/strands_entraid_onenote.py
* fix formatting
* Update 05-blueprints/shopping-concierge-agent/tests/utils.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
* removed tests folders.
* remove info logging
* remove logging
* codeql suppressions
* Update server.py
# codeql[py/clear-text-logging-sensitive-data] Debug logging for certificate verification - logs metadata only, not private key content
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
* Updating .gitignore and adding lib folder required for the shopping and travel concierge agents
---------
Signed-off-by: Hardik Thakkar <68253981+HardikThakkar94@users.noreply.github.com>
Co-authored-by: HT <hardikvt@amazon.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -49,6 +49,7 @@ downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
!05-blueprints/**/lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
||||
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
||||
import * as customResources from 'aws-cdk-lib/custom-resources';
|
||||
import { Construct } from 'constructs';
|
||||
import { GatewayConstruct } from './constructs/gateway-construct';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Sanitize names for AgentCore resources (alphanumeric + underscores)
|
||||
const sanitizeName = (name: string) =>
|
||||
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class AgentStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Import Cognito from Amplify
|
||||
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
|
||||
const clientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-ClientId`);
|
||||
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
|
||||
const discoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
|
||||
|
||||
// Import machine client ID from Amplify (created there for proper deployment order)
|
||||
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
|
||||
|
||||
// Import the user pool for OAuth provider secret retrieval
|
||||
const userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', userPoolId);
|
||||
|
||||
// Import DynamoDB tables
|
||||
const userProfileTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`);
|
||||
const wishlistTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`);
|
||||
const feedbackTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-FeedbackTableName`);
|
||||
|
||||
// Import MCP runtime ARNs (using deployment ID in stack names)
|
||||
const cartRuntimeArn = cdk.Fn.importValue(`CartStack-${DEPLOYMENT_ID}-RuntimeArn`);
|
||||
const shoppingRuntimeArn = cdk.Fn.importValue(`ShoppingStack-${DEPLOYMENT_ID}-RuntimeArn`);
|
||||
|
||||
// 1. Create Custom Resource Lambda for OAuth Provider
|
||||
const oauthProviderRole = new iam.Role(this, 'OAuthProviderLambdaRole', {
|
||||
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
||||
description: 'Execution role for OAuth Provider Custom Resource Lambda',
|
||||
managedPolicies: [
|
||||
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
|
||||
],
|
||||
inlinePolicies: {
|
||||
OAuthProviderPolicy: new iam.PolicyDocument({
|
||||
statements: [
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:CreateOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:DeleteOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:ListOAuth2CredentialProviders',
|
||||
'bedrock-agentcore:GetOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:CreateTokenVault', // Required for OAuth provider creation
|
||||
'bedrock-agentcore:DeleteTokenVault',
|
||||
'bedrock-agentcore:GetTokenVault',
|
||||
'secretsmanager:CreateSecret', // Required to store OAuth client secret
|
||||
'secretsmanager:DeleteSecret',
|
||||
'secretsmanager:GetSecretValue',
|
||||
'secretsmanager:PutSecretValue',
|
||||
'cognito-idp:DescribeUserPoolClient' // Required to fetch client secret
|
||||
],
|
||||
resources: ['*']
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const oauthProviderLambda = new lambda.Function(this, 'OAuthProviderLambda', {
|
||||
runtime: lambda.Runtime.PYTHON_3_13,
|
||||
handler: 'index.handler',
|
||||
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'oauth-provider')),
|
||||
timeout: cdk.Duration.minutes(5),
|
||||
role: oauthProviderRole,
|
||||
description: 'Custom Resource for OAuth2 Credential Provider management'
|
||||
});
|
||||
|
||||
const oauthProvider = new customResources.Provider(this, 'OAuthProvider', {
|
||||
onEventHandler: oauthProviderLambda
|
||||
});
|
||||
|
||||
// 2. Create OAuth Provider via Custom Resource
|
||||
const oauthCredentialProvider = new cdk.CustomResource(this, 'OAuthCredentialProvider', {
|
||||
serviceToken: oauthProvider.serviceToken,
|
||||
properties: {
|
||||
ProviderName: sanitizeName(`oauth_provider_${this.stackName}`),
|
||||
UserPoolId: userPoolId,
|
||||
ClientId: machineClientId,
|
||||
DiscoveryUrl: discoveryUrl,
|
||||
Version: '2' // Increment to force update
|
||||
}
|
||||
});
|
||||
|
||||
const oauthProviderArn = oauthCredentialProvider.getAttString('ProviderArn');
|
||||
|
||||
// 1. Create Memory using L2 construct
|
||||
const memory = new agentcore.Memory(this, 'Memory', {
|
||||
memoryName: sanitizeName(`memory_${this.stackName}`),
|
||||
description: 'Short-term memory for Concierge Agent',
|
||||
});
|
||||
|
||||
// 2. Create execution role for main agent
|
||||
const agentRole = new iam.Role(this, 'AgentRole', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: 'Execution role for Concierge Agent'
|
||||
});
|
||||
|
||||
// Grant DynamoDB permissions
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['dynamodb:GetItem', 'dynamodb:Scan', 'dynamodb:UpdateItem', 'dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:DeleteItem', 'dynamodb:BatchWriteItem'],
|
||||
resources: [
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}/index/*`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}/index/*`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}/index/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Grant CloudWatch Logs permissions
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:GetLogEvents', 'logs:FilterLogEvents'],
|
||||
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:*`]
|
||||
}));
|
||||
|
||||
// Grant Memory access
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'bedrock-agentcore:GetMemory',
|
||||
'bedrock-agentcore:ListMemories',
|
||||
'bedrock-agentcore:CreateEvent',
|
||||
'bedrock-agentcore:GetEvent',
|
||||
'bedrock-agentcore:ListEvents',
|
||||
'bedrock-agentcore:RetrieveMemoryRecords'
|
||||
],
|
||||
resources: [memory.memoryArn]
|
||||
}));
|
||||
|
||||
// Grant Bedrock model access
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Grant Gateway invoke permissions (for runtime to call gateway)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock-agentcore:InvokeGateway'],
|
||||
resources: ['*'] // Will be restricted to specific gateway after creation
|
||||
}));
|
||||
|
||||
// Grant ECR permissions (required for runtime to pull container image)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'ecr:GetAuthorizationToken',
|
||||
'ecr:BatchCheckLayerAvailability',
|
||||
'ecr:GetDownloadUrlForLayer',
|
||||
'ecr:BatchGetImage'
|
||||
],
|
||||
resources: ['*']
|
||||
}));
|
||||
|
||||
// Grant Cognito permissions (required for runtime to fetch machine client secret and domain)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'cognito-idp:DescribeUserPoolClient',
|
||||
'cognito-idp:DescribeUserPool'
|
||||
],
|
||||
resources: [`arn:aws:cognito-idp:${this.region}:${this.account}:userpool/${userPoolId}`]
|
||||
}));
|
||||
|
||||
// 3. Create main agent runtime using L2 construct
|
||||
const baseEnvVars = {
|
||||
MEMORY_ID: memory.memoryId,
|
||||
USER_PROFILE_TABLE_NAME: userProfileTableName,
|
||||
WISHLIST_TABLE_NAME: wishlistTableName,
|
||||
FEEDBACK_TABLE_NAME: feedbackTableName,
|
||||
DEPLOYMENT_ID: DEPLOYMENT_ID,
|
||||
// Gateway M2M authentication credentials (for runtime to call gateway)
|
||||
GATEWAY_CLIENT_ID: machineClientId,
|
||||
GATEWAY_USER_POOL_ID: userPoolId,
|
||||
GATEWAY_SCOPE: 'concierge-gateway/invoke',
|
||||
};
|
||||
|
||||
// Create runtime first (without GATEWAY_URL since gateway doesn't exist yet)
|
||||
const runtime = new agentcore.Runtime(this, 'Runtime', {
|
||||
runtimeName: sanitizeName(`agent_${this.stackName}`),
|
||||
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
|
||||
path.join(__dirname, '../../..', 'concierge_agent', 'supervisor_agent')
|
||||
),
|
||||
executionRole: agentRole,
|
||||
protocolConfiguration: agentcore.ProtocolType.HTTP,
|
||||
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
|
||||
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingJWT(
|
||||
discoveryUrl,
|
||||
[clientId, machineClientId] // Accept user tokens (frontend) AND M2M tokens (gateway)
|
||||
),
|
||||
environmentVariables: baseEnvVars,
|
||||
description: 'Concierge Agent Runtime'
|
||||
});
|
||||
|
||||
// 6. Create Gateway with JWT inbound and OAuth outbound
|
||||
// IMPORTANT: Gateway must use SAME M2M client as runtime and OAuth provider
|
||||
// Inbound: Frontend authenticates to gateway using M2M JWT token
|
||||
// Outbound: Gateway authenticates to targets using OAuth (same M2M client)
|
||||
const gateway = new GatewayConstruct(this, 'Gateway', {
|
||||
gatewayName: sanitizeName(`gateway_${this.stackName}`).replace(/_/g, '-'),
|
||||
mcpRuntimeArns: [
|
||||
{ name: 'CartTools', arn: cartRuntimeArn },
|
||||
{ name: 'ShoppingTools', arn: shoppingRuntimeArn }
|
||||
],
|
||||
cognitoClientId: machineClientId, // Use M2M client (same as runtime and OAuth)
|
||||
cognitoDiscoveryUrl: discoveryUrl,
|
||||
oauthProviderArn: oauthProviderArn,
|
||||
oauthScope: 'concierge-gateway/invoke'
|
||||
});
|
||||
|
||||
// Store gateway URL in SSM Parameter Store for runtime to access
|
||||
const gatewayUrlParameter = new cdk.aws_ssm.StringParameter(this, 'GatewayUrlParameter', {
|
||||
parameterName: `/concierge-agent/${DEPLOYMENT_ID}/gateway-url`,
|
||||
stringValue: gateway.gatewayUrl,
|
||||
description: 'AgentCore Gateway URL for supervisor agent',
|
||||
tier: cdk.aws_ssm.ParameterTier.STANDARD,
|
||||
});
|
||||
|
||||
// Grant runtime permission to read SSM parameter
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['ssm:GetParameter'],
|
||||
resources: [gatewayUrlParameter.parameterArn]
|
||||
}));
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'MainRuntimeArn', {
|
||||
value: runtime.agentRuntimeArn,
|
||||
exportName: `${this.stackName}-MainRuntimeArn`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MainRuntimeId', {
|
||||
value: runtime.agentRuntimeId,
|
||||
exportName: `${this.stackName}-MainRuntimeId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MemoryId', {
|
||||
value: memory.memoryId,
|
||||
exportName: `${this.stackName}-MemoryId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayUrl', {
|
||||
value: gateway.gatewayUrl,
|
||||
exportName: `${this.stackName}-GatewayUrl`,
|
||||
description: 'Gateway URL for MCP client connections (IAM auth)'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayId', {
|
||||
value: gateway.gatewayId,
|
||||
exportName: `${this.stackName}-GatewayId`,
|
||||
description: 'Gateway ID'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayArn', {
|
||||
value: gateway.gatewayArn,
|
||||
exportName: `${this.stackName}-GatewayArn`,
|
||||
description: 'Gateway ARN'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayTargetCount', {
|
||||
value: gateway.targets.length.toString(),
|
||||
description: 'Number of gateway targets configured'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MachineClientId', {
|
||||
value: machineClientId,
|
||||
description: 'Machine client ID for gateway OAuth and MCP authentication (imported from Amplify)'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'OAuthProviderArn', {
|
||||
value: oauthProviderArn,
|
||||
exportName: `${this.stackName}-OAuthProviderArn`,
|
||||
description: 'OAuth provider ARN for gateway targets'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { AgentStack } from './agent-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
new AgentStack(app, `AgentStack-${DEPLOYMENT_ID}`, {
|
||||
env: {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
},
|
||||
description: `Concierge Agent with Gateway - Main agent runtime, memory, and gateway with MCP targets (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export interface GatewayProps {
|
||||
gatewayName: string;
|
||||
mcpRuntimeArns: { name: string; arn: string }[];
|
||||
cognitoClientId: string;
|
||||
cognitoDiscoveryUrl: string;
|
||||
oauthProviderArn: string;
|
||||
oauthScope: string;
|
||||
}
|
||||
|
||||
export class GatewayConstruct extends Construct {
|
||||
public readonly gateway: bedrockagentcore.CfnGateway;
|
||||
public readonly gatewayArn: string;
|
||||
public readonly gatewayId: string;
|
||||
public readonly gatewayUrl: string;
|
||||
public readonly targets: bedrockagentcore.CfnGatewayTarget[];
|
||||
|
||||
constructor(scope: Construct, id: string, props: GatewayProps) {
|
||||
super(scope, id);
|
||||
|
||||
const stack = cdk.Stack.of(this);
|
||||
|
||||
// Gateway Role with permissions to invoke runtimes and Lambda functions
|
||||
const gatewayRole = new iam.Role(this, 'GatewayRole', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: 'Execution role for AgentCore Gateway',
|
||||
inlinePolicies: {
|
||||
GatewayPolicy: new iam.PolicyDocument({
|
||||
statements: [
|
||||
// Bedrock AgentCore permissions (only if there are MCP runtimes)
|
||||
...(props.mcpRuntimeArns.length > 0 ? [new iam.PolicyStatement({
|
||||
sid: 'BedrockAgentCoreAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:InvokeRuntime',
|
||||
'bedrock-agentcore:InvokeRuntimeWithResponseStream'
|
||||
],
|
||||
resources: [
|
||||
...props.mcpRuntimeArns.map(r => r.arn)
|
||||
],
|
||||
})] : []),
|
||||
// Lambda invoke permissions (for future Lambda targets)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'LambdaInvokeAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['lambda:InvokeFunction'],
|
||||
resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*`],
|
||||
}),
|
||||
// Bedrock model access (for gateway operations)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'BedrockModelAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${stack.account}:inference-profile/*`
|
||||
],
|
||||
}),
|
||||
// CloudWatch Logs
|
||||
new iam.PolicyStatement({
|
||||
sid: 'CloudWatchLogsAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'logs:CreateLogGroup',
|
||||
'logs:CreateLogStream',
|
||||
'logs:PutLogEvents'
|
||||
],
|
||||
resources: [`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/bedrock-agentcore/*`],
|
||||
}),
|
||||
// OAuth Provider access (for authenticating to MCP servers)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'OAuthProviderAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:GetOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:GetTokenVault',
|
||||
'bedrock-agentcore:GetWorkloadAccessToken', // Required for OAuth workload identity
|
||||
'bedrock-agentcore:GetResourceOauth2Token', // Required to get OAuth token from provider
|
||||
'secretsmanager:GetSecretValue'
|
||||
],
|
||||
resources: [
|
||||
props.oauthProviderArn,
|
||||
`arn:aws:secretsmanager:${stack.region}:${stack.account}:secret:*`,
|
||||
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:workload-identity-directory/*`,
|
||||
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:token-vault/*`
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Create Gateway with JWT inbound auth
|
||||
this.gateway = new bedrockagentcore.CfnGateway(this, 'Gateway', {
|
||||
name: props.gatewayName,
|
||||
roleArn: gatewayRole.roleArn,
|
||||
protocolType: 'MCP',
|
||||
protocolConfiguration: {
|
||||
mcp: {
|
||||
supportedVersions: ['2025-03-26']
|
||||
// searchType removed - cannot be updated when targets are attached
|
||||
}
|
||||
},
|
||||
authorizerType: 'CUSTOM_JWT',
|
||||
authorizerConfiguration: {
|
||||
customJwtAuthorizer: {
|
||||
allowedClients: [props.cognitoClientId],
|
||||
discoveryUrl: props.cognitoDiscoveryUrl
|
||||
}
|
||||
},
|
||||
description: 'AgentCore Gateway with MCP protocol, JWT inbound auth, and OAuth outbound auth'
|
||||
});
|
||||
|
||||
this.gatewayArn = this.gateway.attrGatewayArn;
|
||||
this.gatewayId = this.gateway.attrGatewayIdentifier;
|
||||
this.gatewayUrl = this.gateway.attrGatewayUrl;
|
||||
|
||||
// Create targets for MCP servers only
|
||||
// Note: Main runtime calls the gateway, not the other way around
|
||||
this.targets = [];
|
||||
|
||||
// MCP server targets with OAuth credentials
|
||||
props.mcpRuntimeArns.forEach((mcpRuntime, index) => {
|
||||
const mcpRuntimeUrl = this.constructRuntimeUrl(mcpRuntime.arn, stack.region);
|
||||
const mcpTarget = new bedrockagentcore.CfnGatewayTarget(this, `McpTarget${index}`, {
|
||||
gatewayIdentifier: this.gatewayId,
|
||||
name: mcpRuntime.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
||||
description: `MCP Server: ${mcpRuntime.name}`,
|
||||
targetConfiguration: {
|
||||
mcp: {
|
||||
mcpServer: {
|
||||
endpoint: mcpRuntimeUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
credentialProviderConfigurations: [{
|
||||
credentialProviderType: 'OAUTH',
|
||||
credentialProvider: {
|
||||
oauthCredentialProvider: {
|
||||
providerArn: props.oauthProviderArn,
|
||||
scopes: [props.oauthScope]
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
mcpTarget.addDependency(this.gateway);
|
||||
this.targets.push(mcpTarget);
|
||||
});
|
||||
|
||||
// Note: Outputs are created in the parent stack to avoid duplicate exports
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the runtime invocation URL with properly encoded ARN
|
||||
* Format: https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded-arn}/invocations
|
||||
*
|
||||
* Note: ARN encoding is handled by CloudFormation Fn::Join with URL-encoded characters
|
||||
*/
|
||||
private constructRuntimeUrl(runtimeArn: string, region: string): string {
|
||||
// Use CloudFormation Fn::Join to construct URL with encoded ARN
|
||||
// The ARN needs to be URL-encoded: : becomes %3A and / becomes %2F
|
||||
return cdk.Fn.join('', [
|
||||
`https://bedrock-agentcore.${region}.amazonaws.com/runtimes/`,
|
||||
// Split ARN by : and / and rejoin with encoded versions
|
||||
cdk.Fn.join('%2F', cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', runtimeArn)))),
|
||||
'/invocations'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { FrontendStack } from './frontend-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
new FrontendStack(app, `FrontendStack-${DEPLOYMENT_ID}`, {
|
||||
env: {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
},
|
||||
description: `Concierge Agent Frontend - Amplify Hosting (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as amplify from '@aws-cdk/aws-amplify-alpha';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class FrontendStack extends cdk.Stack {
|
||||
public readonly amplifyApp: amplify.App;
|
||||
public readonly amplifyUrl: string;
|
||||
public readonly stagingBucket: s3.Bucket;
|
||||
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Create staging bucket for Amplify deployments
|
||||
this.stagingBucket = new s3.Bucket(this, 'StagingBucket', {
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
autoDeleteObjects: true,
|
||||
versioned: true,
|
||||
publicReadAccess: false,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
lifecycleRules: [
|
||||
{
|
||||
id: 'DeleteOldDeployments',
|
||||
enabled: true,
|
||||
expiration: cdk.Duration.days(30),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Add bucket policy for Amplify access
|
||||
this.stagingBucket.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: 'AmplifyAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
principals: [new iam.ServicePrincipal('amplify.amazonaws.com')],
|
||||
actions: ['s3:GetObject', 's3:GetObjectVersion'],
|
||||
resources: [this.stagingBucket.arnForObjects('*')],
|
||||
})
|
||||
);
|
||||
|
||||
// Create Amplify app
|
||||
this.amplifyApp = new amplify.App(this, 'AmplifyApp', {
|
||||
appName: 'concierge-agent-frontend',
|
||||
description: 'Concierge Agent - React/Vite Frontend',
|
||||
platform: amplify.Platform.WEB,
|
||||
});
|
||||
|
||||
// Create main branch
|
||||
this.amplifyApp.addBranch('main', {
|
||||
stage: 'PRODUCTION',
|
||||
branchName: 'main',
|
||||
});
|
||||
|
||||
// Predictable domain format
|
||||
this.amplifyUrl = `https://main.${this.amplifyApp.appId}.amplifyapp.com`;
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'AmplifyAppId', {
|
||||
value: this.amplifyApp.appId,
|
||||
description: 'Amplify App ID',
|
||||
exportName: `${this.stackName}-AmplifyAppId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'AmplifyUrl', {
|
||||
value: this.amplifyUrl,
|
||||
description: 'Amplify App URL'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'StagingBucketName', {
|
||||
value: this.stagingBucket.bucketName,
|
||||
description: 'S3 Staging Bucket Name',
|
||||
exportName: `${this.stackName}-StagingBucketName`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { CartStack } from './cart-stack';
|
||||
import { ShoppingStack } from './shopping-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
const env = {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
};
|
||||
|
||||
new CartStack(app, `CartStack-${DEPLOYMENT_ID}`, {
|
||||
env,
|
||||
description: `Cart MCP Server - Shopping cart tools (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
new ShoppingStack(app, `ShoppingStack-${DEPLOYMENT_ID}`, {
|
||||
env,
|
||||
description: `Shopping MCP Server - Product search tools (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Sanitize names for AgentCore resources (alphanumeric + underscores only)
|
||||
const sanitizeName = (name: string) =>
|
||||
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export interface BaseMcpStackProps extends cdk.StackProps {
|
||||
mcpName: string;
|
||||
agentCodePath: string;
|
||||
environmentVariables?: { [key: string]: string };
|
||||
additionalPolicies?: iam.PolicyStatement[];
|
||||
ssmParameters?: string[];
|
||||
}
|
||||
|
||||
export abstract class BaseMcpStack extends cdk.Stack {
|
||||
public readonly runtime: agentcore.Runtime;
|
||||
public readonly role: iam.Role;
|
||||
|
||||
constructor(scope: Construct, id: string, props: BaseMcpStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Import Cognito configuration from Amplify stack
|
||||
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
|
||||
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
|
||||
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
|
||||
const cognitoDiscoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
|
||||
|
||||
// Create execution role
|
||||
this.role = new iam.Role(this, 'Role', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: `Execution role for ${props.mcpName} MCP server`
|
||||
});
|
||||
|
||||
// Add base CloudWatch Logs permissions
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'CloudWatchLogs',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'logs:CreateLogGroup',
|
||||
'logs:CreateLogStream',
|
||||
'logs:PutLogEvents'
|
||||
],
|
||||
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`]
|
||||
}));
|
||||
|
||||
// Add Bedrock model invocation permissions (including cross-region inference)
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'BedrockModelInvoke',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock:InvokeModel',
|
||||
'bedrock:InvokeModelWithResponseStream'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Add additional policies if provided
|
||||
if (props.additionalPolicies) {
|
||||
props.additionalPolicies.forEach(policy => this.role.addToPolicy(policy));
|
||||
}
|
||||
|
||||
// Add SSM parameter read access if provided
|
||||
if (props.ssmParameters && props.ssmParameters.length > 0) {
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'SSMParameterAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
|
||||
resources: props.ssmParameters.map(param =>
|
||||
`arn:aws:ssm:${this.region}:${this.account}:parameter${param}`
|
||||
)
|
||||
}));
|
||||
|
||||
// Add KMS decrypt permission for SecureString parameters
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'KMSDecrypt',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['kms:Decrypt'],
|
||||
resources: [`arn:aws:kms:${this.region}:${this.account}:key/*`],
|
||||
conditions: {
|
||||
StringEquals: {
|
||||
'kms:ViaService': `ssm.${this.region}.amazonaws.com`
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Build environment variables
|
||||
const envVars = {
|
||||
AWS_REGION: this.region,
|
||||
...props.environmentVariables
|
||||
};
|
||||
|
||||
// Create MCP Runtime with OAuth authentication
|
||||
this.runtime = new agentcore.Runtime(this, 'Runtime', {
|
||||
runtimeName: sanitizeName(`${props.mcpName}_mcp_${this.stackName}`),
|
||||
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
|
||||
path.join(__dirname, '../../..', props.agentCodePath) // nosemgrep
|
||||
),
|
||||
executionRole: this.role,
|
||||
protocolConfiguration: agentcore.ProtocolType.MCP,
|
||||
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
|
||||
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingOAuth(
|
||||
cognitoDiscoveryUrl,
|
||||
machineClientId
|
||||
),
|
||||
environmentVariables: envVars,
|
||||
description: `MCP Server: ${props.mcpName} (OAuth enabled)`
|
||||
});
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'RuntimeArn', {
|
||||
value: this.runtime.agentRuntimeArn,
|
||||
description: `${props.mcpName} MCP Runtime ARN`,
|
||||
exportName: `${this.stackName}-RuntimeArn`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'RuntimeId', {
|
||||
value: this.runtime.agentRuntimeId,
|
||||
description: `${props.mcpName} MCP Runtime ID`,
|
||||
exportName: `${this.stackName}-RuntimeId`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import { BaseMcpStack } from './base-mcp-stack';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class CartStack extends BaseMcpStack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, {
|
||||
...props,
|
||||
mcpName: 'cart',
|
||||
agentCodePath: 'concierge_agent/mcp_cart_tools',
|
||||
ssmParameters: [],
|
||||
environmentVariables: {
|
||||
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
|
||||
WISHLIST_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`)
|
||||
},
|
||||
additionalPolicies: [
|
||||
new iam.PolicyStatement({
|
||||
sid: 'DynamoDBAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'dynamodb:GetItem',
|
||||
'dynamodb:PutItem',
|
||||
'dynamodb:UpdateItem',
|
||||
'dynamodb:DeleteItem',
|
||||
'dynamodb:Query',
|
||||
'dynamodb:Scan'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:dynamodb:*:*:table/*`
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add Visa secrets policy after super() so we can use 'this'
|
||||
this.runtime.addToRolePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: 'VisaSecretsAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'secretsmanager:GetSecretValue'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import { Construct } from 'constructs';
|
||||
import { BaseMcpStack } from './base-mcp-stack';
|
||||
|
||||
export class ShoppingStack extends BaseMcpStack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, {
|
||||
...props,
|
||||
mcpName: 'shopping',
|
||||
agentCodePath: 'concierge_agent/mcp_shopping_tools',
|
||||
ssmParameters: [
|
||||
'/concierge-agent/shopping/serp-api-key'
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
||||
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export class VisaLambdaStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const deploymentId = this.node.tryGetContext('deploymentId') || 'default';
|
||||
|
||||
// Lambda function for Visa proxy - using standard Python runtime with ZIP deployment
|
||||
const visaLambda = new lambda.Function(this, 'VisaProxyLambda', {
|
||||
runtime: lambda.Runtime.PYTHON_3_11,
|
||||
handler: 'handler.handler',
|
||||
code: lambda.Code.fromAsset('../../concierge_agent/local-visa-server', {
|
||||
bundling: {
|
||||
image: lambda.Runtime.PYTHON_3_11.bundlingImage,
|
||||
command: [
|
||||
'bash', '-c',
|
||||
[
|
||||
// Install dependencies with correct platform binaries
|
||||
'pip install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: --target /asset-output',
|
||||
// Copy Python source files (exclude __pycache__)
|
||||
'cp *.py /asset-output/',
|
||||
// Copy visa module (exclude __pycache__)
|
||||
'rsync -av --exclude="__pycache__" --exclude="*.pyc" --exclude="*.pyo" visa/ /asset-output/visa/',
|
||||
// Copy config directory
|
||||
'cp -r config /asset-output/',
|
||||
].join(' && ')
|
||||
],
|
||||
},
|
||||
}),
|
||||
timeout: cdk.Duration.seconds(30),
|
||||
memorySize: 512,
|
||||
environment: {
|
||||
CLIENT_APP_ID: 'VICTestAccountTR',
|
||||
// AWS_REGION is automatically set by Lambda runtime
|
||||
},
|
||||
description: 'Visa Payment API Proxy Lambda',
|
||||
});
|
||||
|
||||
// Grant Secrets Manager access for Visa credentials
|
||||
visaLambda.addToRolePolicy(new iam.PolicyStatement({
|
||||
actions: ['secretsmanager:GetSecretValue'],
|
||||
resources: [
|
||||
// Allow access to visa/* secrets (where actual Visa credentials are stored)
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`,
|
||||
// Also allow access to deployment-specific secrets if they exist
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:concierge-agent/${deploymentId}/visa/*`,
|
||||
],
|
||||
}));
|
||||
|
||||
// API Gateway REST API
|
||||
const api = new apigateway.RestApi(this, 'VisaProxyApi', {
|
||||
restApiName: 'Visa Proxy API',
|
||||
description: 'Proxy for Visa Payment API calls',
|
||||
deployOptions: {
|
||||
stageName: 'prod',
|
||||
tracingEnabled: true,
|
||||
loggingLevel: apigateway.MethodLoggingLevel.INFO,
|
||||
metricsEnabled: true,
|
||||
},
|
||||
defaultCorsPreflightOptions: {
|
||||
allowOrigins: apigateway.Cors.ALL_ORIGINS, // Allow all origins for simplicity
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
|
||||
allowCredentials: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Add /api/visa/* proxy resource
|
||||
const apiResource = api.root.addResource('api');
|
||||
const visaResource = apiResource.addResource('visa');
|
||||
|
||||
// Add proxy+ to catch all paths under /api/visa/
|
||||
visaResource.addProxy({
|
||||
defaultIntegration: new apigateway.LambdaIntegration(visaLambda, {
|
||||
timeout: cdk.Duration.seconds(29),
|
||||
proxy: true,
|
||||
}),
|
||||
anyMethod: true,
|
||||
});
|
||||
|
||||
// Add CORS headers to Gateway error responses (4XX, 5XX)
|
||||
// This ensures CORS headers are present even when Lambda returns errors
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': "'*'",
|
||||
'Access-Control-Allow-Headers': "'Content-Type,Authorization'",
|
||||
'Access-Control-Allow-Methods': "'GET,POST,OPTIONS'",
|
||||
};
|
||||
|
||||
// Add CORS to all error response types
|
||||
api.addGatewayResponse('Default4xx', {
|
||||
type: apigateway.ResponseType.DEFAULT_4XX,
|
||||
responseHeaders: corsHeaders,
|
||||
});
|
||||
|
||||
api.addGatewayResponse('Default5xx', {
|
||||
type: apigateway.ResponseType.DEFAULT_5XX,
|
||||
responseHeaders: corsHeaders,
|
||||
});
|
||||
|
||||
// CloudFormation Outputs
|
||||
new cdk.CfnOutput(this, 'VisaProxyApiUrl', {
|
||||
value: api.url,
|
||||
description: 'Visa Proxy API Gateway URL (use this in frontend VITE_VISA_PROXY_URL)',
|
||||
exportName: `VisaProxyApiUrl-${deploymentId}`,
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'VisaLambdaFunctionName', {
|
||||
value: visaLambda.functionName,
|
||||
description: 'Visa Lambda Function Name',
|
||||
exportName: `VisaLambdaFunctionName-${deploymentId}`,
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'VisaLambdaArn', {
|
||||
value: visaLambda.functionArn,
|
||||
description: 'Visa Lambda Function ARN',
|
||||
exportName: `VisaLambdaArn-${deploymentId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
||||
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
||||
import * as customResources from 'aws-cdk-lib/custom-resources';
|
||||
import { Construct } from 'constructs';
|
||||
import { GatewayConstruct } from './constructs/gateway-construct';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Sanitize names for AgentCore resources (alphanumeric + underscores)
|
||||
const sanitizeName = (name: string) =>
|
||||
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class AgentStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Import Cognito from Amplify
|
||||
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
|
||||
const clientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-ClientId`);
|
||||
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
|
||||
const discoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
|
||||
|
||||
// Import machine client ID from Amplify (created there for proper deployment order)
|
||||
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
|
||||
|
||||
// Import the user pool for OAuth provider secret retrieval
|
||||
const userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', userPoolId);
|
||||
|
||||
// Import DynamoDB tables
|
||||
const userProfileTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`);
|
||||
const wishlistTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`);
|
||||
const itineraryTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-ItineraryTableName`);
|
||||
const feedbackTableName = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-FeedbackTableName`);
|
||||
|
||||
// Import MCP runtime ARNs (using deployment ID in stack names)
|
||||
const travelRuntimeArn = cdk.Fn.importValue(`TravelStack-${DEPLOYMENT_ID}-RuntimeArn`);
|
||||
const cartRuntimeArn = cdk.Fn.importValue(`CartStack-${DEPLOYMENT_ID}-RuntimeArn`);
|
||||
const itineraryRuntimeArn = cdk.Fn.importValue(`ItineraryStack-${DEPLOYMENT_ID}-RuntimeArn`);
|
||||
|
||||
// 1. Create Custom Resource Lambda for OAuth Provider
|
||||
const oauthProviderRole = new iam.Role(this, 'OAuthProviderLambdaRole', {
|
||||
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
||||
description: 'Execution role for OAuth Provider Custom Resource Lambda',
|
||||
managedPolicies: [
|
||||
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
|
||||
],
|
||||
inlinePolicies: {
|
||||
OAuthProviderPolicy: new iam.PolicyDocument({
|
||||
statements: [
|
||||
new iam.PolicyStatement({
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:CreateOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:DeleteOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:ListOAuth2CredentialProviders',
|
||||
'bedrock-agentcore:GetOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:CreateTokenVault', // Required for OAuth provider creation
|
||||
'bedrock-agentcore:DeleteTokenVault',
|
||||
'bedrock-agentcore:GetTokenVault',
|
||||
'secretsmanager:CreateSecret', // Required to store OAuth client secret
|
||||
'secretsmanager:DeleteSecret',
|
||||
'secretsmanager:GetSecretValue',
|
||||
'secretsmanager:PutSecretValue',
|
||||
'cognito-idp:DescribeUserPoolClient' // Required to fetch client secret
|
||||
],
|
||||
resources: ['*']
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const oauthProviderLambda = new lambda.Function(this, 'OAuthProviderLambda', {
|
||||
runtime: lambda.Runtime.PYTHON_3_13,
|
||||
handler: 'index.handler',
|
||||
code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambdas', 'oauth-provider')),
|
||||
timeout: cdk.Duration.minutes(5),
|
||||
role: oauthProviderRole,
|
||||
description: 'Custom Resource for OAuth2 Credential Provider management'
|
||||
});
|
||||
|
||||
const oauthProvider = new customResources.Provider(this, 'OAuthProvider', {
|
||||
onEventHandler: oauthProviderLambda
|
||||
});
|
||||
|
||||
// 2. Create OAuth Provider via Custom Resource
|
||||
const oauthCredentialProvider = new cdk.CustomResource(this, 'OAuthCredentialProvider', {
|
||||
serviceToken: oauthProvider.serviceToken,
|
||||
properties: {
|
||||
ProviderName: sanitizeName(`oauth_provider_${this.stackName}`),
|
||||
UserPoolId: userPoolId,
|
||||
ClientId: machineClientId,
|
||||
DiscoveryUrl: discoveryUrl,
|
||||
Version: '2' // Increment to force update
|
||||
}
|
||||
});
|
||||
|
||||
const oauthProviderArn = oauthCredentialProvider.getAttString('ProviderArn');
|
||||
|
||||
// 1. Create Memory using L2 construct
|
||||
const memory = new agentcore.Memory(this, 'Memory', {
|
||||
memoryName: sanitizeName(`memory_${this.stackName}`),
|
||||
description: 'Short-term memory for Concierge Agent',
|
||||
});
|
||||
|
||||
// 2. Create execution role for main agent
|
||||
const agentRole = new iam.Role(this, 'AgentRole', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: 'Execution role for Concierge Agent'
|
||||
});
|
||||
|
||||
// Grant DynamoDB permissions
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['dynamodb:GetItem', 'dynamodb:Scan', 'dynamodb:UpdateItem', 'dynamodb:Query', 'dynamodb:PutItem', 'dynamodb:DeleteItem', 'dynamodb:BatchWriteItem'],
|
||||
resources: [
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${userProfileTableName}/index/*`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${wishlistTableName}/index/*`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${itineraryTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${itineraryTableName}/index/*`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}`,
|
||||
`arn:aws:dynamodb:${this.region}:${this.account}:table/${feedbackTableName}/index/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Grant CloudWatch Logs permissions
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents', 'logs:DescribeLogGroups', 'logs:DescribeLogStreams', 'logs:GetLogEvents', 'logs:FilterLogEvents'],
|
||||
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:*`]
|
||||
}));
|
||||
|
||||
// Grant Memory access
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'bedrock-agentcore:GetMemory',
|
||||
'bedrock-agentcore:ListMemories',
|
||||
'bedrock-agentcore:CreateEvent',
|
||||
'bedrock-agentcore:GetEvent',
|
||||
'bedrock-agentcore:ListEvents',
|
||||
'bedrock-agentcore:RetrieveMemoryRecords'
|
||||
],
|
||||
resources: [memory.memoryArn]
|
||||
}));
|
||||
|
||||
// Grant Bedrock model access
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Grant Gateway invoke permissions (for runtime to call gateway)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock-agentcore:InvokeGateway'],
|
||||
resources: ['*'] // Will be restricted to specific gateway after creation
|
||||
}));
|
||||
|
||||
// Grant ECR permissions (required for runtime to pull container image)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'ecr:GetAuthorizationToken',
|
||||
'ecr:BatchCheckLayerAvailability',
|
||||
'ecr:GetDownloadUrlForLayer',
|
||||
'ecr:BatchGetImage'
|
||||
],
|
||||
resources: ['*']
|
||||
}));
|
||||
|
||||
// Grant Cognito permissions (required for runtime to fetch machine client secret and domain)
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: [
|
||||
'cognito-idp:DescribeUserPoolClient',
|
||||
'cognito-idp:DescribeUserPool'
|
||||
],
|
||||
resources: [`arn:aws:cognito-idp:${this.region}:${this.account}:userpool/${userPoolId}`]
|
||||
}));
|
||||
|
||||
// 3. Create main agent runtime using L2 construct
|
||||
const baseEnvVars = {
|
||||
MEMORY_ID: memory.memoryId,
|
||||
USER_PROFILE_TABLE_NAME: userProfileTableName,
|
||||
WISHLIST_TABLE_NAME: wishlistTableName,
|
||||
ITINERARY_TABLE_NAME: itineraryTableName,
|
||||
FEEDBACK_TABLE_NAME: feedbackTableName,
|
||||
DEPLOYMENT_ID: DEPLOYMENT_ID,
|
||||
// Gateway M2M authentication credentials (for runtime to call gateway)
|
||||
GATEWAY_CLIENT_ID: machineClientId,
|
||||
GATEWAY_USER_POOL_ID: userPoolId,
|
||||
GATEWAY_SCOPE: 'concierge-gateway/invoke',
|
||||
};
|
||||
|
||||
// Create runtime first (without GATEWAY_URL since gateway doesn't exist yet)
|
||||
const runtime = new agentcore.Runtime(this, 'Runtime', {
|
||||
runtimeName: sanitizeName(`agent_${this.stackName}`),
|
||||
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
|
||||
path.join(__dirname, '../../..', 'concierge_agent', 'supervisor_agent')
|
||||
),
|
||||
executionRole: agentRole,
|
||||
protocolConfiguration: agentcore.ProtocolType.HTTP,
|
||||
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
|
||||
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingJWT(
|
||||
discoveryUrl,
|
||||
[clientId, machineClientId] // Accept user tokens (frontend) AND M2M tokens (gateway)
|
||||
),
|
||||
environmentVariables: baseEnvVars,
|
||||
description: 'Concierge Agent Runtime'
|
||||
});
|
||||
|
||||
// 6. Create Gateway with JWT inbound and OAuth outbound
|
||||
// IMPORTANT: Gateway must use SAME M2M client as runtime and OAuth provider
|
||||
// Inbound: Frontend authenticates to gateway using M2M JWT token
|
||||
// Outbound: Gateway authenticates to targets using OAuth (same M2M client)
|
||||
const gateway = new GatewayConstruct(this, 'Gateway', {
|
||||
gatewayName: sanitizeName(`gateway_${this.stackName}`).replace(/_/g, '-'),
|
||||
mcpRuntimeArns: [
|
||||
{ name: 'CartTools', arn: cartRuntimeArn },
|
||||
{ name: 'ItineraryTools', arn: itineraryRuntimeArn },
|
||||
{ name: 'TravelTools', arn: travelRuntimeArn }
|
||||
],
|
||||
cognitoClientId: machineClientId, // Use M2M client (same as runtime and OAuth)
|
||||
cognitoDiscoveryUrl: discoveryUrl,
|
||||
oauthProviderArn: oauthProviderArn,
|
||||
oauthScope: 'concierge-gateway/invoke'
|
||||
});
|
||||
|
||||
// Store gateway URL in SSM Parameter Store for runtime to access
|
||||
const gatewayUrlParameter = new cdk.aws_ssm.StringParameter(this, 'GatewayUrlParameter', {
|
||||
parameterName: `/concierge-agent/${DEPLOYMENT_ID}/gateway-url`,
|
||||
stringValue: gateway.gatewayUrl,
|
||||
description: 'AgentCore Gateway URL for supervisor agent',
|
||||
tier: cdk.aws_ssm.ParameterTier.STANDARD,
|
||||
});
|
||||
|
||||
// Grant runtime permission to read SSM parameter
|
||||
agentRole.addToPolicy(new iam.PolicyStatement({
|
||||
actions: ['ssm:GetParameter'],
|
||||
resources: [gatewayUrlParameter.parameterArn]
|
||||
}));
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'MainRuntimeArn', {
|
||||
value: runtime.agentRuntimeArn,
|
||||
exportName: `${this.stackName}-MainRuntimeArn`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MainRuntimeId', {
|
||||
value: runtime.agentRuntimeId,
|
||||
exportName: `${this.stackName}-MainRuntimeId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MemoryId', {
|
||||
value: memory.memoryId,
|
||||
exportName: `${this.stackName}-MemoryId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayUrl', {
|
||||
value: gateway.gatewayUrl,
|
||||
exportName: `${this.stackName}-GatewayUrl`,
|
||||
description: 'Gateway URL for MCP client connections (IAM auth)'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayId', {
|
||||
value: gateway.gatewayId,
|
||||
exportName: `${this.stackName}-GatewayId`,
|
||||
description: 'Gateway ID'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayArn', {
|
||||
value: gateway.gatewayArn,
|
||||
exportName: `${this.stackName}-GatewayArn`,
|
||||
description: 'Gateway ARN'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'GatewayTargetCount', {
|
||||
value: gateway.targets.length.toString(),
|
||||
description: 'Number of gateway targets configured'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'MachineClientId', {
|
||||
value: machineClientId,
|
||||
description: 'Machine client ID for gateway OAuth and MCP authentication (imported from Amplify)'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'OAuthProviderArn', {
|
||||
value: oauthProviderArn,
|
||||
exportName: `${this.stackName}-OAuthProviderArn`,
|
||||
description: 'OAuth provider ARN for gateway targets'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { AgentStack } from './agent-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
new AgentStack(app, `AgentStack-${DEPLOYMENT_ID}`, {
|
||||
env: {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
},
|
||||
description: `Concierge Agent with Gateway - Main agent runtime, memory, and gateway with MCP targets (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export interface GatewayProps {
|
||||
gatewayName: string;
|
||||
mcpRuntimeArns: { name: string; arn: string }[];
|
||||
cognitoClientId: string;
|
||||
cognitoDiscoveryUrl: string;
|
||||
oauthProviderArn: string;
|
||||
oauthScope: string;
|
||||
}
|
||||
|
||||
export class GatewayConstruct extends Construct {
|
||||
public readonly gateway: bedrockagentcore.CfnGateway;
|
||||
public readonly gatewayArn: string;
|
||||
public readonly gatewayId: string;
|
||||
public readonly gatewayUrl: string;
|
||||
public readonly targets: bedrockagentcore.CfnGatewayTarget[];
|
||||
|
||||
constructor(scope: Construct, id: string, props: GatewayProps) {
|
||||
super(scope, id);
|
||||
|
||||
const stack = cdk.Stack.of(this);
|
||||
|
||||
// Gateway Role with permissions to invoke runtimes and Lambda functions
|
||||
const gatewayRole = new iam.Role(this, 'GatewayRole', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: 'Execution role for AgentCore Gateway',
|
||||
inlinePolicies: {
|
||||
GatewayPolicy: new iam.PolicyDocument({
|
||||
statements: [
|
||||
// Bedrock AgentCore permissions (only if there are MCP runtimes)
|
||||
...(props.mcpRuntimeArns.length > 0 ? [new iam.PolicyStatement({
|
||||
sid: 'BedrockAgentCoreAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:InvokeRuntime',
|
||||
'bedrock-agentcore:InvokeRuntimeWithResponseStream'
|
||||
],
|
||||
resources: [
|
||||
...props.mcpRuntimeArns.map(r => r.arn)
|
||||
],
|
||||
})] : []),
|
||||
// Lambda invoke permissions (for future Lambda targets)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'LambdaInvokeAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['lambda:InvokeFunction'],
|
||||
resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*`],
|
||||
}),
|
||||
// Bedrock model access (for gateway operations)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'BedrockModelAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${stack.account}:inference-profile/*`
|
||||
],
|
||||
}),
|
||||
// CloudWatch Logs
|
||||
new iam.PolicyStatement({
|
||||
sid: 'CloudWatchLogsAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'logs:CreateLogGroup',
|
||||
'logs:CreateLogStream',
|
||||
'logs:PutLogEvents'
|
||||
],
|
||||
resources: [`arn:aws:logs:${stack.region}:${stack.account}:log-group:/aws/bedrock-agentcore/*`],
|
||||
}),
|
||||
// OAuth Provider access (for authenticating to MCP servers)
|
||||
new iam.PolicyStatement({
|
||||
sid: 'OAuthProviderAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock-agentcore:GetOAuth2CredentialProvider',
|
||||
'bedrock-agentcore:GetTokenVault',
|
||||
'bedrock-agentcore:GetWorkloadAccessToken', // Required for OAuth workload identity
|
||||
'bedrock-agentcore:GetResourceOauth2Token', // Required to get OAuth token from provider
|
||||
'secretsmanager:GetSecretValue'
|
||||
],
|
||||
resources: [
|
||||
props.oauthProviderArn,
|
||||
`arn:aws:secretsmanager:${stack.region}:${stack.account}:secret:*`,
|
||||
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:workload-identity-directory/*`,
|
||||
`arn:aws:bedrock-agentcore:${stack.region}:${stack.account}:token-vault/*`
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Create Gateway with JWT inbound auth
|
||||
this.gateway = new bedrockagentcore.CfnGateway(this, 'Gateway', {
|
||||
name: props.gatewayName,
|
||||
roleArn: gatewayRole.roleArn,
|
||||
protocolType: 'MCP',
|
||||
protocolConfiguration: {
|
||||
mcp: {
|
||||
supportedVersions: ['2025-03-26']
|
||||
}
|
||||
},
|
||||
authorizerType: 'CUSTOM_JWT',
|
||||
authorizerConfiguration: {
|
||||
customJwtAuthorizer: {
|
||||
allowedClients: [props.cognitoClientId],
|
||||
discoveryUrl: props.cognitoDiscoveryUrl
|
||||
}
|
||||
},
|
||||
description: 'AgentCore Gateway with MCP protocol, JWT inbound auth, and OAuth outbound auth'
|
||||
});
|
||||
|
||||
this.gatewayArn = this.gateway.attrGatewayArn;
|
||||
this.gatewayId = this.gateway.attrGatewayIdentifier;
|
||||
this.gatewayUrl = this.gateway.attrGatewayUrl;
|
||||
|
||||
// Create targets for MCP servers only
|
||||
// Note: Main runtime calls the gateway, not the other way around
|
||||
this.targets = [];
|
||||
|
||||
// MCP server targets with OAuth credentials
|
||||
props.mcpRuntimeArns.forEach((mcpRuntime, index) => {
|
||||
const mcpRuntimeUrl = this.constructRuntimeUrl(mcpRuntime.arn, stack.region);
|
||||
const mcpTarget = new bedrockagentcore.CfnGatewayTarget(this, `McpTarget${index}`, {
|
||||
gatewayIdentifier: this.gatewayId,
|
||||
name: mcpRuntime.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
||||
description: `MCP Server: ${mcpRuntime.name}`,
|
||||
targetConfiguration: {
|
||||
mcp: {
|
||||
mcpServer: {
|
||||
endpoint: mcpRuntimeUrl
|
||||
}
|
||||
}
|
||||
},
|
||||
credentialProviderConfigurations: [{
|
||||
credentialProviderType: 'OAUTH',
|
||||
credentialProvider: {
|
||||
oauthCredentialProvider: {
|
||||
providerArn: props.oauthProviderArn,
|
||||
scopes: [props.oauthScope]
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
mcpTarget.addDependency(this.gateway);
|
||||
this.targets.push(mcpTarget);
|
||||
});
|
||||
|
||||
// Note: Outputs are created in the parent stack to avoid duplicate exports
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the runtime invocation URL with properly encoded ARN
|
||||
* Format: https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded-arn}/invocations
|
||||
*
|
||||
* Note: ARN encoding is handled by CloudFormation Fn::Join with URL-encoded characters
|
||||
*/
|
||||
private constructRuntimeUrl(runtimeArn: string, region: string): string {
|
||||
// Use CloudFormation Fn::Join to construct URL with encoded ARN
|
||||
// The ARN needs to be URL-encoded: : becomes %3A and / becomes %2F
|
||||
return cdk.Fn.join('', [
|
||||
`https://bedrock-agentcore.${region}.amazonaws.com/runtimes/`,
|
||||
// Split ARN by : and / and rejoin with encoded versions
|
||||
cdk.Fn.join('%2F', cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', runtimeArn)))),
|
||||
'/invocations'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { FrontendStack } from './frontend-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
new FrontendStack(app, `FrontendStack-${DEPLOYMENT_ID}`, {
|
||||
env: {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
},
|
||||
description: `Concierge Agent Frontend - Amplify Hosting (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as amplify from '@aws-cdk/aws-amplify-alpha';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class FrontendStack extends cdk.Stack {
|
||||
public readonly amplifyApp: amplify.App;
|
||||
public readonly amplifyUrl: string;
|
||||
public readonly stagingBucket: s3.Bucket;
|
||||
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Create staging bucket for Amplify deployments
|
||||
this.stagingBucket = new s3.Bucket(this, 'StagingBucket', {
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
autoDeleteObjects: true,
|
||||
versioned: true,
|
||||
publicReadAccess: false,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
lifecycleRules: [
|
||||
{
|
||||
id: 'DeleteOldDeployments',
|
||||
enabled: true,
|
||||
expiration: cdk.Duration.days(30),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Add bucket policy for Amplify access
|
||||
this.stagingBucket.addToResourcePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: 'AmplifyAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
principals: [new iam.ServicePrincipal('amplify.amazonaws.com')],
|
||||
actions: ['s3:GetObject', 's3:GetObjectVersion'],
|
||||
resources: [this.stagingBucket.arnForObjects('*')],
|
||||
})
|
||||
);
|
||||
|
||||
// Create Amplify app
|
||||
this.amplifyApp = new amplify.App(this, 'AmplifyApp', {
|
||||
appName: 'concierge-agent-frontend',
|
||||
description: 'Concierge Agent - React/Vite Frontend',
|
||||
platform: amplify.Platform.WEB,
|
||||
});
|
||||
|
||||
// Create main branch
|
||||
this.amplifyApp.addBranch('main', {
|
||||
stage: 'PRODUCTION',
|
||||
branchName: 'main',
|
||||
});
|
||||
|
||||
// Predictable domain format
|
||||
this.amplifyUrl = `https://main.${this.amplifyApp.appId}.amplifyapp.com`;
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'AmplifyAppId', {
|
||||
value: this.amplifyApp.appId,
|
||||
description: 'Amplify App ID',
|
||||
exportName: `${this.stackName}-AmplifyAppId`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'AmplifyUrl', {
|
||||
value: this.amplifyUrl,
|
||||
description: 'Amplify App URL'
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'StagingBucketName', {
|
||||
value: this.stagingBucket.bucketName,
|
||||
description: 'S3 Staging Bucket Name',
|
||||
exportName: `${this.stackName}-StagingBucketName`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as fs from 'fs';
|
||||
import { TravelStack } from './travel-stack';
|
||||
import { CartStack } from './cart-stack';
|
||||
import { ItineraryStack } from './itinerary-stack';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
const env = {
|
||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||
region: process.env.CDK_DEFAULT_REGION || 'us-east-1'
|
||||
};
|
||||
|
||||
new TravelStack(app, `TravelStack-${DEPLOYMENT_ID}`, {
|
||||
env,
|
||||
description: `Travel MCP Server - Travel search tools (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
new CartStack(app, `CartStack-${DEPLOYMENT_ID}`, {
|
||||
env,
|
||||
description: `Cart MCP Server - Shopping cart tools (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
new ItineraryStack(app, `ItineraryStack-${DEPLOYMENT_ID}`, {
|
||||
env,
|
||||
description: `Itinerary MCP Server - Itinerary management tools (${DEPLOYMENT_ID})`
|
||||
});
|
||||
|
||||
app.synth();
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Sanitize names for AgentCore resources (alphanumeric + underscores only)
|
||||
const sanitizeName = (name: string) =>
|
||||
name.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_');
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export interface BaseMcpStackProps extends cdk.StackProps {
|
||||
mcpName: string;
|
||||
agentCodePath: string;
|
||||
environmentVariables?: { [key: string]: string };
|
||||
additionalPolicies?: iam.PolicyStatement[];
|
||||
ssmParameters?: string[];
|
||||
}
|
||||
|
||||
export abstract class BaseMcpStack extends cdk.Stack {
|
||||
public readonly runtime: agentcore.Runtime;
|
||||
public readonly role: iam.Role;
|
||||
|
||||
constructor(scope: Construct, id: string, props: BaseMcpStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// Add deployment info
|
||||
cdk.Tags.of(this).add('DeploymentId', DEPLOYMENT_ID);
|
||||
cdk.Tags.of(this).add('DeploymentName', deploymentConfig.deploymentName || DEPLOYMENT_ID);
|
||||
|
||||
// Import Cognito configuration from Amplify stack
|
||||
const userPoolId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-UserPoolId`);
|
||||
const cognitoRegion = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-Region`);
|
||||
const machineClientId = cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Auth-MachineClientId`);
|
||||
const cognitoDiscoveryUrl = `https://cognito-idp.${cognitoRegion}.amazonaws.com/${userPoolId}/.well-known/openid-configuration`;
|
||||
|
||||
// Create execution role
|
||||
this.role = new iam.Role(this, 'Role', {
|
||||
assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
|
||||
description: `Execution role for ${props.mcpName} MCP server`
|
||||
});
|
||||
|
||||
// Add base CloudWatch Logs permissions
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'CloudWatchLogs',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'logs:CreateLogGroup',
|
||||
'logs:CreateLogStream',
|
||||
'logs:PutLogEvents'
|
||||
],
|
||||
resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`]
|
||||
}));
|
||||
|
||||
// Add Bedrock model invocation permissions (including cross-region inference)
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'BedrockModelInvoke',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'bedrock:InvokeModel',
|
||||
'bedrock:InvokeModelWithResponseStream'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:bedrock:*::foundation-model/*`,
|
||||
`arn:aws:bedrock:*:${this.account}:inference-profile/*`
|
||||
]
|
||||
}));
|
||||
|
||||
// Add additional policies if provided
|
||||
if (props.additionalPolicies) {
|
||||
props.additionalPolicies.forEach(policy => this.role.addToPolicy(policy));
|
||||
}
|
||||
|
||||
// Add SSM parameter read access if provided
|
||||
if (props.ssmParameters && props.ssmParameters.length > 0) {
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'SSMParameterAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
|
||||
resources: props.ssmParameters.map(param =>
|
||||
`arn:aws:ssm:${this.region}:${this.account}:parameter${param}`
|
||||
)
|
||||
}));
|
||||
|
||||
// Add KMS decrypt permission for SecureString parameters
|
||||
this.role.addToPolicy(new iam.PolicyStatement({
|
||||
sid: 'KMSDecrypt',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: ['kms:Decrypt'],
|
||||
resources: [`arn:aws:kms:${this.region}:${this.account}:key/*`],
|
||||
conditions: {
|
||||
StringEquals: {
|
||||
'kms:ViaService': `ssm.${this.region}.amazonaws.com`
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Build environment variables
|
||||
const envVars = {
|
||||
AWS_REGION: this.region,
|
||||
...props.environmentVariables
|
||||
};
|
||||
|
||||
// Create MCP Runtime with OAuth authentication
|
||||
this.runtime = new agentcore.Runtime(this, 'Runtime', {
|
||||
runtimeName: sanitizeName(`${props.mcpName}_mcp_${this.stackName}`),
|
||||
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset(
|
||||
path.join(__dirname, '../../..', props.agentCodePath) // nosemgrep
|
||||
),
|
||||
executionRole: this.role,
|
||||
protocolConfiguration: agentcore.ProtocolType.MCP,
|
||||
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
|
||||
authorizerConfiguration: agentcore.RuntimeAuthorizerConfiguration.usingOAuth(
|
||||
cognitoDiscoveryUrl,
|
||||
machineClientId
|
||||
),
|
||||
environmentVariables: envVars,
|
||||
description: `MCP Server: ${props.mcpName} (OAuth enabled)`
|
||||
});
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'RuntimeArn', {
|
||||
value: this.runtime.agentRuntimeArn,
|
||||
description: `${props.mcpName} MCP Runtime ARN`,
|
||||
exportName: `${this.stackName}-RuntimeArn`
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'RuntimeId', {
|
||||
value: this.runtime.agentRuntimeId,
|
||||
description: `${props.mcpName} MCP Runtime ID`,
|
||||
exportName: `${this.stackName}-RuntimeId`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import { BaseMcpStack } from './base-mcp-stack';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class CartStack extends BaseMcpStack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, {
|
||||
...props,
|
||||
mcpName: 'cart',
|
||||
agentCodePath: 'concierge_agent/mcp_cart_tools',
|
||||
ssmParameters: [],
|
||||
environmentVariables: {
|
||||
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
|
||||
WISHLIST_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-WishlistTableName`)
|
||||
},
|
||||
additionalPolicies: [
|
||||
new iam.PolicyStatement({
|
||||
sid: 'DynamoDBAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'dynamodb:GetItem',
|
||||
'dynamodb:PutItem',
|
||||
'dynamodb:UpdateItem',
|
||||
'dynamodb:DeleteItem',
|
||||
'dynamodb:Query',
|
||||
'dynamodb:Scan'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:dynamodb:*:*:table/*`
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Add Visa secrets policy after super() so we can use 'this'
|
||||
this.runtime.addToRolePolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: 'VisaSecretsAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'secretsmanager:GetSecretValue'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
import { BaseMcpStack } from './base-mcp-stack';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Load deployment config
|
||||
const deploymentConfig = JSON.parse(fs.readFileSync('../../deployment-config.json', 'utf-8'));
|
||||
const DEPLOYMENT_ID = deploymentConfig.deploymentId;
|
||||
|
||||
export class ItineraryStack extends BaseMcpStack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, {
|
||||
...props,
|
||||
mcpName: 'itinerary',
|
||||
agentCodePath: 'concierge_agent/mcp_itinerary_tools',
|
||||
ssmParameters: [],
|
||||
environmentVariables: {
|
||||
USER_PROFILE_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-UserProfileTableName`),
|
||||
ITINERARY_TABLE_NAME: cdk.Fn.importValue(`ConciergeAgent-${DEPLOYMENT_ID}-Data-ItineraryTableName`)
|
||||
},
|
||||
additionalPolicies: [
|
||||
new iam.PolicyStatement({
|
||||
sid: 'DynamoDBAccess',
|
||||
effect: iam.Effect.ALLOW,
|
||||
actions: [
|
||||
'dynamodb:GetItem',
|
||||
'dynamodb:PutItem',
|
||||
'dynamodb:UpdateItem',
|
||||
'dynamodb:DeleteItem',
|
||||
'dynamodb:Query',
|
||||
'dynamodb:Scan'
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:dynamodb:*:*:table/*`
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import { Construct } from 'constructs';
|
||||
import { BaseMcpStack } from './base-mcp-stack';
|
||||
|
||||
export class TravelStack extends BaseMcpStack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, {
|
||||
...props,
|
||||
mcpName: 'travel',
|
||||
agentCodePath: 'concierge_agent/mcp_travel_tools',
|
||||
ssmParameters: [
|
||||
'/concierge-agent/travel/openweather-api-key',
|
||||
'/concierge-agent/travel/tavily-api-key',
|
||||
'/concierge-agent/travel/serp-api-key',
|
||||
'/concierge-agent/travel/google-maps-key',
|
||||
'/concierge-agent/travel/amadeus-public',
|
||||
'/concierge-agent/travel/amadeus-secret'
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
||||
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export class VisaLambdaStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const deploymentId = this.node.tryGetContext('deploymentId') || 'default';
|
||||
|
||||
// Lambda function for Visa proxy - using standard Python runtime with ZIP deployment
|
||||
const visaLambda = new lambda.Function(this, 'VisaProxyLambda', {
|
||||
runtime: lambda.Runtime.PYTHON_3_11,
|
||||
handler: 'handler.handler',
|
||||
code: lambda.Code.fromAsset('../../concierge_agent/local-visa-server', {
|
||||
bundling: {
|
||||
image: lambda.Runtime.PYTHON_3_11.bundlingImage,
|
||||
command: [
|
||||
'bash', '-c',
|
||||
[
|
||||
// Install dependencies with correct platform binaries
|
||||
'pip install -r requirements.txt --platform manylinux2014_x86_64 --only-binary=:all: --target /asset-output',
|
||||
// Copy Python source files (exclude __pycache__)
|
||||
'cp *.py /asset-output/',
|
||||
// Copy visa module (exclude __pycache__)
|
||||
'rsync -av --exclude="__pycache__" --exclude="*.pyc" --exclude="*.pyo" visa/ /asset-output/visa/',
|
||||
// Copy config directory
|
||||
'cp -r config /asset-output/',
|
||||
].join(' && ')
|
||||
],
|
||||
},
|
||||
}),
|
||||
timeout: cdk.Duration.seconds(30),
|
||||
memorySize: 512,
|
||||
environment: {
|
||||
CLIENT_APP_ID: 'VICTestAccountTR',
|
||||
// AWS_REGION is automatically set by Lambda runtime
|
||||
},
|
||||
description: 'Visa Payment API Proxy Lambda',
|
||||
});
|
||||
|
||||
// Grant Secrets Manager access for Visa credentials
|
||||
visaLambda.addToRolePolicy(new iam.PolicyStatement({
|
||||
actions: ['secretsmanager:GetSecretValue'],
|
||||
resources: [
|
||||
// Allow access to visa/* secrets (where actual Visa credentials are stored)
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:visa/*`,
|
||||
// Also allow access to deployment-specific secrets if they exist
|
||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:concierge-agent/${deploymentId}/visa/*`,
|
||||
],
|
||||
}));
|
||||
|
||||
// API Gateway REST API
|
||||
const api = new apigateway.RestApi(this, 'VisaProxyApi', {
|
||||
restApiName: 'Visa Proxy API',
|
||||
description: 'Proxy for Visa Payment API calls',
|
||||
deployOptions: {
|
||||
stageName: 'prod',
|
||||
tracingEnabled: true,
|
||||
loggingLevel: apigateway.MethodLoggingLevel.INFO,
|
||||
metricsEnabled: true,
|
||||
},
|
||||
defaultCorsPreflightOptions: {
|
||||
allowOrigins: apigateway.Cors.ALL_ORIGINS, // Allow all origins for simplicity
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
|
||||
allowCredentials: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Add /api/visa/* proxy resource
|
||||
const apiResource = api.root.addResource('api');
|
||||
const visaResource = apiResource.addResource('visa');
|
||||
|
||||
// Add proxy+ to catch all paths under /api/visa/
|
||||
visaResource.addProxy({
|
||||
defaultIntegration: new apigateway.LambdaIntegration(visaLambda, {
|
||||
timeout: cdk.Duration.seconds(29),
|
||||
proxy: true,
|
||||
}),
|
||||
anyMethod: true,
|
||||
});
|
||||
|
||||
// Add CORS headers to Gateway error responses (4XX, 5XX)
|
||||
// This ensures CORS headers are present even when Lambda returns errors
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': "'*'",
|
||||
'Access-Control-Allow-Headers': "'Content-Type,Authorization'",
|
||||
'Access-Control-Allow-Methods': "'GET,POST,OPTIONS'",
|
||||
};
|
||||
|
||||
// Add CORS to all error response types
|
||||
api.addGatewayResponse('Default4xx', {
|
||||
type: apigateway.ResponseType.DEFAULT_4XX,
|
||||
responseHeaders: corsHeaders,
|
||||
});
|
||||
|
||||
api.addGatewayResponse('Default5xx', {
|
||||
type: apigateway.ResponseType.DEFAULT_5XX,
|
||||
responseHeaders: corsHeaders,
|
||||
});
|
||||
|
||||
// CloudFormation Outputs
|
||||
new cdk.CfnOutput(this, 'VisaProxyApiUrl', {
|
||||
value: api.url,
|
||||
description: 'Visa Proxy API Gateway URL (use this in frontend VITE_VISA_PROXY_URL)',
|
||||
exportName: `VisaProxyApiUrl-${deploymentId}`,
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'VisaLambdaFunctionName', {
|
||||
value: visaLambda.functionName,
|
||||
description: 'Visa Lambda Function Name',
|
||||
exportName: `VisaLambdaFunctionName-${deploymentId}`,
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'VisaLambdaArn', {
|
||||
value: visaLambda.functionArn,
|
||||
description: 'Visa Lambda Function ARN',
|
||||
exportName: `VisaLambdaArn-${deploymentId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user