import { AgentCoreApplication, AgentCoreMcp, AgentCorePaymentManager, AgentCorePaymentConnector, type AgentCoreProjectSpec, type AgentCoreMcpSpec, type CustomJWTAuthorizerConfig, type HarnessDeploymentConfig, } from '@aws/agentcore-cdk'; import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib'; import * as iam from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; /** * Harness deployment config: role-scoped fields (for IAM role + container build) * plus the full validated spec + its config directory so the L3 construct can * synthesize the AWS::BedrockAgentCore::Harness resource. */ export type HarnessConfig = HarnessDeploymentConfig; export interface ManualPaymentConnectorSpec { name: string; provider: 'CoinbaseCDP' | 'StripePrivy'; provisionMode?: 'MANUAL'; credentialName: string; credentialProviderArn: string; } export interface QuickCreatePaymentConnectorSpec { name: string; provider: 'CoinbaseCDP'; provisionMode: 'QUICK_CREATE'; credentialName?: never; credentialProviderArn?: never; } export type PaymentConnectorSpec = ManualPaymentConnectorSpec | QuickCreatePaymentConnectorSpec; export interface PaymentSpec { name: string; description?: string; authorizerType: 'AWS_IAM' | 'CUSTOM_JWT'; authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig }; autoPayment?: boolean; paymentToolAllowlist?: string[]; networkPreferences?: string[]; connectors: PaymentConnectorSpec[]; } export interface AgentCoreStackProps extends StackProps { /** * The AgentCore project specification containing agents, memories, and credentials. */ spec: AgentCoreProjectSpec; /** * The MCP specification containing gateways and servers. */ mcpSpec?: AgentCoreMcpSpec; /** * Credential provider ARNs from deployed state, keyed by credential name. */ credentials?: Record; /** * Harness role configurations. */ harnesses?: HarnessConfig[]; /** * Parsed connectorParameters for non-S3 KB data sources, keyed by * connectorConfigFile path. Forwarded to AgentCoreApplication. */ connectorParametersByFile?: Record>; /** * Payment specifications with resolved credential provider ARNs. */ paymentSpec?: PaymentSpec[]; } function toCdkId(name: string): string { return name.replace(/_/g, ''); } /** * Decide whether a deployed runtime should receive payment env vars + IAM grants. * Payments today only ships a runtime shim for Python HTTP runtimes; injecting * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes * would surface env vars they cannot consume and would dilute least-privilege * IAM grants for runtimes that never call ProcessPayment. */ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean { if (agent.protocol && agent.protocol !== 'HTTP') { return false; } const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : ''; const entrypointFile = entrypoint.split(':')[0] ?? ''; return entrypointFile.endsWith('.py'); } /** * CDK Stack that deploys AgentCore infrastructure. * * This is a thin wrapper that instantiates L3 constructs. * All resource logic and outputs are contained within the L3 constructs. */ export class AgentCoreStack extends Stack { /** The AgentCore application containing all agent environments */ public readonly application: AgentCoreApplication; constructor(scope: Construct, id: string, props: AgentCoreStackProps) { super(scope, id, props); const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props; // Create AgentCoreApplication with all agents and harness roles // eslint-disable-next-line @typescript-eslint/no-explicit-any const appProps: Record = { spec }; if (harnesses?.length) { appProps.harnesses = harnesses; } if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) { appProps.connectorParametersByFile = connectorParametersByFile; } if (credentials) { appProps.credentials = credentials; } this.application = new AgentCoreApplication(this, 'Application', appProps as any); // Create AgentCoreMcp if there are gateways configured if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) { new AgentCoreMcp(this, 'Mcp', { projectName: spec.name, mcpSpec, agentCoreApplication: this.application, credentials, projectTags: spec.tags, }); } // Create payment infrastructure via CFN constructs if (paymentSpec && paymentSpec.length > 0) { for (const payment of paymentSpec) { const mgrId = toCdkId(payment.name); const manager = new AgentCorePaymentManager(this, `Payment${mgrId}`, { projectName: spec.name, name: payment.name, authorizerType: payment.authorizerType, description: payment.description, authorizerConfiguration: payment.authorizerConfiguration, tags: spec.tags, }); const prefix = `AGENTCORE_PAYMENT_${payment.name.toUpperCase().replace(/-/g, '_')}`; // Wire env vars from construct output tokens into eligible agent environments only. // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that // can consume these env vars, and giving them sts:AssumeRole on the // ProcessPaymentRole would broaden the privilege surface unnecessarily. for (const env of this.application.environments.values()) { if (!isPaymentEligibleAgent(env.agent)) { continue; } env.runtime.addEnvironmentVariable(`${prefix}_MANAGER_ARN`, manager.paymentManagerArn); env.runtime.addEnvironmentVariable(`${prefix}_PROCESS_PAYMENT_ROLE_ARN`, manager.processPaymentRoleArn); // Grant runtime execution role permission to assume the ProcessPaymentRole. // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the // caller still needs sts:AssumeRole on its own role to perform the assumption. env.runtime.role.addToPrincipalPolicy( new iam.PolicyStatement({ actions: ['sts:AssumeRole'], resources: [manager.processPaymentRoleArn], }) ); // Grant payment data-plane actions directly to the runtime role. // // NOTE: This deviates from the canonical role model in the AgentCore Payments // beta guide, which assigns Get/List/Create instrument+session actions to a // separate ManagementRole and limits the agent's role to ProcessPayment only. // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header) // calls GetPaymentInstrument internally during the 402 auto-pay path, so the // runtime role needs read access. CreatePaymentSession is included so // `agentcore invoke --auto-session` works without a separate ManagementRole // call. Tighten this if the SDK is updated to accept pre-fetched instrument // details and split create-session into a backend-only flow. env.runtime.role.addToPrincipalPolicy( new iam.PolicyStatement({ actions: [ 'bedrock-agentcore:GetPaymentInstrument', 'bedrock-agentcore:ListPaymentInstruments', 'bedrock-agentcore:GetPaymentInstrumentBalance', 'bedrock-agentcore:GetPaymentSession', 'bedrock-agentcore:ListPaymentSessions', 'bedrock-agentcore:CreatePaymentSession', 'bedrock-agentcore:ProcessPayment', ], resources: [manager.paymentManagerArn, `${manager.paymentManagerArn}/*`], }) ); if (payment.autoPayment !== undefined) { env.runtime.addEnvironmentVariable(`${prefix}_AUTO_PAYMENT`, String(payment.autoPayment)); } if (payment.paymentToolAllowlist) { env.runtime.addEnvironmentVariable(`${prefix}_TOOL_ALLOWLIST`, payment.paymentToolAllowlist.join(',')); } if (payment.networkPreferences) { env.runtime.addEnvironmentVariable(`${prefix}_NETWORK_PREFERENCES`, payment.networkPreferences.join(',')); } if (payment.authorizerType === 'CUSTOM_JWT') { env.runtime.addEnvironmentVariable(`${prefix}_AUTH_MODE`, 'bearer'); } } // Create connectors for this manager for (const connector of payment.connectors) { const connId = toCdkId(connector.name); const schemaConnector = connector.provisionMode === 'QUICK_CREATE' ? connector : { name: connector.name, provider: connector.provider, ...(connector.provisionMode && { provisionMode: connector.provisionMode }), credentialName: connector.credentialName, }; const compatibilityProps = { projectName: spec.name, paymentManager: manager, connector: schemaConnector, // Remove these legacy manual fields after the new L3 release is pinned. connectorName: connector.name, connectorType: connector.provider, ...(connector.provisionMode !== 'QUICK_CREATE' && { credentialProviderArn: connector.credentialProviderArn, }), }; const conn = new AgentCorePaymentConnector( this, `Payment${mgrId}${connId}`, compatibilityProps as unknown as ConstructorParameters[2] ); // Wire first connector's ID as env var (eligible agents only) if (connector === payment.connectors[0]) { for (const env of this.application.environments.values()) { if (!isPaymentEligibleAgent(env.agent)) continue; env.runtime.addEnvironmentVariable(`${prefix}_CONNECTOR_ID`, conn.paymentConnectorId); } } new CfnOutput(this, `Payment${mgrId}${connId}ConnectorId`, { value: conn.paymentConnectorId, }); if (connector.provisionMode === 'QUICK_CREATE') { const quickCreateConnector = conn as AgentCorePaymentConnector & { paymentConnectorStatus: string; authorizationUrl: string; }; new CfnOutput(this, `Payment${mgrId}${connId}ConnectorStatus`, { value: quickCreateConnector.paymentConnectorStatus, }); new CfnOutput(this, `Payment${mgrId}${connId}AuthorizationUrl`, { value: quickCreateConnector.authorizationUrl, }); } } // CFN Outputs for post-deploy state parsing new CfnOutput(this, `Payment${mgrId}ManagerArn`, { value: manager.paymentManagerArn, }); new CfnOutput(this, `Payment${mgrId}ManagerId`, { value: manager.paymentManagerId, }); new CfnOutput(this, `Payment${mgrId}ProcessPaymentRoleArn`, { value: manager.processPaymentRoleArn, }); new CfnOutput(this, `Payment${mgrId}ResourceRetrievalRoleArn`, { value: manager.resourceRetrievalRoleArn, }); } } // Stack-level output new CfnOutput(this, 'StackNameOutput', { description: 'Name of the CloudFormation Stack', value: this.stackName, }); } }