diff --git a/docs/payments.md b/docs/payments.md index 32b58e8def..c124cef354 100644 --- a/docs/payments.md +++ b/docs/payments.md @@ -487,6 +487,8 @@ configuration. The CLI never imperatively deletes a provider generated by Quick manual connectors: - Credential cross-references: verifies each connector's `credentialName` maps to a valid credential entry +- Credential name format: verifies each connector's `credentialName` uses only letters, numbers, hyphens, and periods, + which the credential provider ARN requires - `.env.local` existence: confirms the secrets file exists when payment connectors are configured - Missing environment variables: checks that all required `AGENTCORE_CREDENTIAL_*` variables are present diff --git a/src/cli/commands/validate/__tests__/action.test.ts b/src/cli/commands/validate/__tests__/action.test.ts index 08e967cdbb..f923599f70 100644 --- a/src/cli/commands/validate/__tests__/action.test.ts +++ b/src/cli/commands/validate/__tests__/action.test.ts @@ -352,6 +352,30 @@ describe('payment validation', () => { expect(result.error.message).toContain('"my-connector"'); }); + it('fails with a rename hint when the credential name contains an underscore', async () => { + mockFindConfigRoot.mockReturnValue(CONFIG_ROOT); + mockReadProjectSpec.mockResolvedValue({ + ...baseSpec, + credentials: [{ ...coinbaseCredential, name: 'PayMgr-my_cdp-cdp' }], + payments: [ + { + name: 'PayMgr', + connectors: [{ name: 'my_cdp', credentialName: 'PayMgr-my_cdp-cdp', provider: 'CoinbaseCDP' }], + }, + ], + }); + mockReadAWSDeploymentTargets.mockResolvedValue([]); + mockConfigExists.mockReturnValue(false); + + const result = await handleValidate({}); + + expect(result.success).toBe(false); + assert(!result.success); + expect(result.error.message).toContain('"my_cdp"'); + expect(result.error.message).toContain('"PayMgr-my_cdp-cdp"'); + expect(result.error.message).toContain('Rename it to "PayMgr-my-cdp-cdp"'); + }); + it('fails when connector provider does not match credential provider', async () => { mockFindConfigRoot.mockReturnValue(CONFIG_ROOT); mockReadProjectSpec.mockResolvedValue({ diff --git a/src/cli/commands/validate/action.ts b/src/cli/commands/validate/action.ts index ce5bb18449..bcb8d2b012 100644 --- a/src/cli/commands/validate/action.ts +++ b/src/cli/commands/validate/action.ts @@ -14,6 +14,7 @@ import { validateHarnessSpecs } from '../../operations/deploy/preflight'; import { computePaymentCredentialEnvVarNames, computeStripePrivyCredentialEnvVarNames, + isPaymentCredentialNameValid, } from '../../primitives/credential-utils'; import { existsSync } from 'fs'; import { join } from 'path'; @@ -88,6 +89,14 @@ export async function handleValidate(options: ValidateOptions): Promise ), }; } + if (!isPaymentCredentialNameValid(connector.credentialName)) { + return { + success: false, + error: new Error( + `Payment connector "${connector.name}" (manager "${payment.name}") uses credential "${connector.credentialName}", but payment connectors can only reference credential names with letters, numbers, hyphens, and periods. Rename it to "${connector.credentialName.replace(/_/g, '-')}" in both \`credentials\` and the connector's \`credentialName\` in agentcore.json; the .env.local variable names stay the same.` + ), + }; + } } } diff --git a/src/cli/primitives/PaymentConnectorPrimitive.ts b/src/cli/primitives/PaymentConnectorPrimitive.ts index 20a2392801..468061c724 100644 --- a/src/cli/primitives/PaymentConnectorPrimitive.ts +++ b/src/cli/primitives/PaymentConnectorPrimitive.ts @@ -117,9 +117,10 @@ export class PaymentConnectorPrimitive extends BasePrimitive { expect(result.credentialName).toBe('mgr1-conn1-cdp'); }); + it('replaces underscores in the connector name so the credential provider ARN stays valid', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject({ payments: [makeManager('mgr1')] })); + + const result = await primitive.add({ + manager: 'mgr1', + name: 'my_cdp', + provider: 'CoinbaseCDP', + apiKeyId: 'key-id', + apiKeySecret: 'key-secret', + walletSecret: 'wallet-secret', + }); + + if (!result.success) throw new Error('expected success'); + expect(result.connectorName).toBe('my_cdp'); + expect(result.credentialName).toBe('mgr1-my-cdp-cdp'); + // Env var names are unchanged: hyphens and underscores both map to "_" + expect(mockSetEnvVar).toHaveBeenCalledWith('AGENTCORE_CREDENTIAL_MGR1_MY_CDP_CDP_API_KEY_ID', 'key-id'); + }); + it('writes all 3 CoinbaseCDP env vars', async () => { mockReadProjectSpec.mockResolvedValue(makeProject({ payments: [makeManager('mgr1')] })); diff --git a/src/cli/primitives/__tests__/credential-utils.test.ts b/src/cli/primitives/__tests__/credential-utils.test.ts index b04424b9da..058b5db123 100644 --- a/src/cli/primitives/__tests__/credential-utils.test.ts +++ b/src/cli/primitives/__tests__/credential-utils.test.ts @@ -2,6 +2,7 @@ import { computeDefaultCredentialEnvVarName, computePaymentCredentialEnvVarNames, computeStripePrivyCredentialEnvVarNames, + isPaymentCredentialNameValid, } from '../credential-utils'; import { describe, expect, it } from 'vitest'; @@ -23,6 +24,17 @@ describe('computeDefaultCredentialEnvVarName', () => { }); }); +describe('isPaymentCredentialNameValid', () => { + it('accepts letters, digits, hyphens and periods', () => { + expect(isPaymentCredentialNameValid('PayMgr-my-cdp-cdp')).toBe(true); + expect(isPaymentCredentialNameValid('mgr1.v2-conn1-stripe-privy')).toBe(true); + }); + + it('rejects underscores', () => { + expect(isPaymentCredentialNameValid('PayMgr-my_cdp-cdp')).toBe(false); + }); +}); + describe('computePaymentCredentialEnvVarNames', () => { it('returns three env var names with correct suffixes', () => { const result = computePaymentCredentialEnvVarNames('myMgr-conn-cdp'); diff --git a/src/cli/primitives/credential-utils.ts b/src/cli/primitives/credential-utils.ts index 6431a0ec64..cdca8adaca 100644 --- a/src/cli/primitives/credential-utils.ts +++ b/src/cli/primitives/credential-utils.ts @@ -16,6 +16,14 @@ export function computeManagedOAuthCredentialName(gatewayName: string): string { return `${gatewayName}-oauth`; } +/** + * Payment connectors reference their credential provider by ARN, and the ARN's name segment only allows + * letters, digits, hyphens and periods. + */ +export function isPaymentCredentialNameValid(credentialName: string): boolean { + return /^[a-zA-Z0-9.-]+$/.test(credentialName); +} + /** * Compute the env var names for a CoinbaseCDP payment credential. * CoinbaseCDP credentials require 3 env vars.