Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/assets/cdk/bin/cdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,19 +146,61 @@ async function main() {

// Extract credentials from deployed state for this target
const targetState = (deployedState as Record<string, unknown>)?.targets as
Record<string, Record<string, unknown>> | undefined;
| Record<string, Record<string, unknown>>
| undefined;
const targetResources = target
? (targetState?.[target.name]?.resources as Record<string, unknown> | undefined)
: undefined;
const credentials = targetResources?.credentials as
Record<string, { credentialProviderArn: string; clientSecretArn?: string }> | undefined;
| Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
| undefined;

// Payment credential provider ARNs live in the same credentials map as identity credentials
const paymentCredentials = credentials;

const paymentSpec = specAny.payments?.length
? specAny.payments.map(
(p: {
name: string;
description?: string;
authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
authorizerConfiguration?: unknown;
autoPayment?: boolean;
paymentToolAllowlist?: string[];
networkPreferences?: string[];
connectors: { name: string; provider?: string; credentialName: string }[];
}) => ({
name: p.name,
description: p.description,
authorizerType: p.authorizerType,
authorizerConfiguration: p.authorizerConfiguration,
autoPayment: p.autoPayment,
paymentToolAllowlist: p.paymentToolAllowlist,
networkPreferences: p.networkPreferences,
connectors: p.connectors.map(c => {
const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
if (!credentialProviderArn) {
// Fail fast with an actionable message rather than passing an empty
// ARN that fails opaquely server-side at CreatePaymentConnector.
throw new Error(
`Payment connector "${c.name}" on manager "${p.name}" references credential ` +
`"${c.credentialName}", but no deployed credential provider was found for it. ` +
`Run \`agentcore deploy\` so the credential provider is created first.`
);
}
return { name: c.name, provider: c.provider, credentialProviderArn };
}),
})
)
: undefined;

new AgentCoreStack(app, stackName, {
spec,
mcpSpec,
credentials,
connectorParametersByFile,
harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
paymentSpec,
env,
description: target
? `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`
Expand Down
169 changes: 162 additions & 7 deletions src/assets/cdk/lib/cdk-stack.ts
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
import {
AgentCoreApplication,
AgentCoreMcp,
AgentCorePayments,
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';

/**
Expand All@@ -16,6 +19,23 @@ import { Construct } from 'constructs';
*/
export type HarnessConfig = HarnessDeploymentConfig;

export interface PaymentConnectorSpec {
name: string;
provider: 'CoinbaseCDP' | 'StripePrivy';
credentialProviderArn: string;
}

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.
Expand All@@ -38,6 +58,30 @@ export interface AgentCoreStackProps extends StackProps {
* 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');
}

/**
Expand All@@ -53,7 +97,7 @@ export class AgentCoreStack extends Stack {
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
super(scope, id, props);

const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile } = 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
Expand All@@ -68,11 +112,6 @@ export class AgentCoreStack extends Stack {
appProps.credentials = credentials;
}
this.application = new AgentCoreApplication(this, 'Application', appProps as any);
new AgentCorePayments(this, 'Payments', {
spec,
credentials,
agentCoreApplication: this.application,
});

// Create AgentCoreMcp if there are gateways configured
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
Expand All@@ -85,6 +124,122 @@ export class AgentCoreStack extends Stack {
});
}

// 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 conn = new AgentCorePaymentConnector(this, `Payment${mgrId}${connId}`, {
projectName: spec.name,
paymentManager: manager,
connectorName: connector.name,
connectorType: connector.provider,
credentialProviderArn: connector.credentialProviderArn,
});

// 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,
});
}

// 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',
Expand Down
2 changes: 1 addition & 1 deletion src/assets/cdk/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@
"typescript": "~5.9.3"
},
"dependencies": {
"@aws/agentcore-cdk": "0.1.0-alpha.49",
"@aws/agentcore-cdk": "0.1.0-alpha.45",
"aws-cdk-lib": "~2.266.0",
"constructs": "~10.7.0"
}
Expand Down
Loading
Loading