nmpAgents/nmpOnboardingAgent/agentcore/cdk/lib/cdk-stack.ts

292 lines
12 KiB
TypeScript

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<string, { credentialProviderArn: string; clientSecretArn?: string }>;
/**
* Harness role configurations.
*/
harnesses?: HarnessConfig[];
/**
* Parsed connectorParameters for non-S3 KB data sources, keyed by
* connectorConfigFile path. Forwarded to AgentCoreApplication.
*/
connectorParametersByFile?: Record<string, Record<string, unknown>>;
/**
* 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<string, unknown> = { 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<typeof AgentCorePaymentConnector>[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,
});
}
}