diff --git a/README.md b/README.md index b257fd1cf..b5a83fb9d 100644 --- a/README.md +++ b/README.md @@ -197,10 +197,10 @@ Policy engines apply Cedar-based pre/post-call policies to agent invocations — Pay-per-call agent transactions via the [x402 protocol](https://www.x402.org/). When a tool call returns `402 Payment Required`, the payments system signs and submits payment then retries automatically. -| Command | Description | -| ----------------------- | ---------------------------------------------------------------------------- | -| `add payment-manager` | Add a payment manager (orchestrates payment sessions for the agent) | -| `add payment-connector` | Add a payment connector with provider credentials (CoinbaseCDP, StripePrivy) | +| Command | Description | +| ----------------------- | ------------------------------------------------------------------------- | +| `add payment-manager` | Add a payment manager (orchestrates payment sessions for the agent) | +| `add payment-connector` | Add a Quick Create or manual payment connector (CoinbaseCDP, StripePrivy) | > See [Payments](docs/payments.md) for the full setup including instrument creation and tool allowlists. diff --git a/docs/PERMISSIONS.md b/docs/PERMISSIONS.md index 231fd13d8..97ccf383f 100644 --- a/docs/PERMISSIONS.md +++ b/docs/PERMISSIONS.md @@ -426,15 +426,16 @@ Required only when the project defines payment managers and connectors (the `pay CLI calls the Payment control-plane and data-plane APIs directly with the developer's credentials; both are signed under the `bedrock-agentcore` service. -| Action | CLI Commands | Purpose | -| --------------------------------------------------- | ------------ | -------------------------------------------------------------------- | -| `bedrock-agentcore:GetPaymentCredentialProvider` | `deploy` | Check if a payment credential provider already exists | -| `bedrock-agentcore:CreatePaymentCredentialProvider` | `deploy` | Create a payment credential provider from connector secrets | -| `bedrock-agentcore:UpdatePaymentCredentialProvider` | `deploy` | Update a payment credential provider with new secret values | -| `bedrock-agentcore:DeletePaymentCredentialProvider` | `deploy` | Remove a payment credential provider when a connector is removed | -| `bedrock-agentcore:GetPaymentManager` | `status` | Look up payment manager status | -| `bedrock-agentcore:ListPaymentSessions` | `invoke` | Find an existing active payment session before creating a new one | -| `bedrock-agentcore:CreatePaymentSession` | `invoke` | Create a payment session with a default budget for `invoke` auto-pay | +| Action | CLI Commands | Purpose | +| --------------------------------------------------- | ------------------ | -------------------------------------------------------------------- | +| `bedrock-agentcore:GetPaymentCredentialProvider` | `deploy` | Check if a payment credential provider already exists | +| `bedrock-agentcore:CreatePaymentCredentialProvider` | `deploy` | Create a payment credential provider from connector secrets | +| `bedrock-agentcore:UpdatePaymentCredentialProvider` | `deploy` | Update a payment credential provider with new secret values | +| `bedrock-agentcore:DeletePaymentCredentialProvider` | `deploy` | Remove a payment credential provider when a connector is removed | +| `bedrock-agentcore:GetPaymentManager` | `status` | Look up payment manager status | +| `bedrock-agentcore:GetPaymentConnector` | `deploy`, `status` | Retrieve connector status and a pending authorization URL | +| `bedrock-agentcore:ListPaymentSessions` | `invoke` | Find an existing active payment session before creating a new one | +| `bedrock-agentcore:CreatePaymentSession` | `invoke` | Create a payment session with a default budget for `invoke` auto-pay | Creating or updating a payment credential provider also writes the connector secrets into a service-managed Secrets Manager secret (named `bedrock-agentcore-identity!default/payment/*`). Unlike API key and OAuth2 providers, the Payment diff --git a/docs/commands.md b/docs/commands.md index 0b12e95e3..2b9520ed3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -521,6 +521,12 @@ agentcore add payment-manager \ Add a payment connector to an existing payment manager. See [Payments](payments.md) for credential details. ```bash +# Quick Create (recommended) +agentcore add payment-connector \ + --manager MyManager \ + --name MyCDPConnector \ + --provision-mode QUICK_CREATE + # CoinbaseCDP provider agentcore add payment-connector \ --manager MyManager \ @@ -541,19 +547,20 @@ agentcore add payment-connector \ --authorization-id your-auth-id ``` -| Flag | Description | -| ----------------------------------- | ------------------------------------------ | -| `--manager ` | Parent payment manager (required) | -| `--name ` | Connector name (required) | -| `--provider ` | `CoinbaseCDP` (default) or `StripePrivy` | -| `--api-key-id ` | Coinbase CDP API Key ID | -| `--api-key-secret ` | Coinbase CDP API Key Secret | -| `--wallet-secret ` | Coinbase CDP Wallet Secret | -| `--app-id ` | Privy App ID (StripePrivy) | -| `--app-secret ` | Privy App Secret (StripePrivy) | -| `--authorization-private-key ` | ECDSA P-256 private key (StripePrivy) | -| `--authorization-id ` | Authorization key identifier (StripePrivy) | -| `--json` | JSON output | +| Flag | Description | +| ----------------------------------- | ------------------------------------------------------ | +| `--manager ` | Parent payment manager (required) | +| `--name ` | Connector name (required) | +| `--provision-mode ` | `QUICK_CREATE` or `MANUAL` (default) | +| `--provider ` | `CoinbaseCDP` or `StripePrivy` (manual mode only) | +| `--api-key-id ` | Coinbase CDP API Key ID (manual mode) | +| `--api-key-secret ` | Coinbase CDP API Key Secret (manual mode) | +| `--wallet-secret ` | Coinbase CDP Wallet Secret (manual mode) | +| `--app-id ` | Privy App ID (StripePrivy manual mode) | +| `--app-secret ` | Privy App Secret (StripePrivy manual mode) | +| `--authorization-private-key ` | ECDSA P-256 private key (StripePrivy manual mode) | +| `--authorization-id ` | Authorization key identifier (StripePrivy manual mode) | +| `--json` | JSON output | ### add credential diff --git a/docs/configuration.md b/docs/configuration.md index e80f4fb68..f6c4e0391 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -526,6 +526,11 @@ wallet credentials. See [Payments](payments.md) for the full usage guide. { "name": "MyCDPConnector", "provider": "CoinbaseCDP", + "provisionMode": "QUICK_CREATE" + }, + { + "name": "MyManualConnector", + "provider": "CoinbaseCDP", "credentialName": "my-cdp-creds" } ] @@ -572,17 +577,18 @@ wallet credentials. See [Payments](payments.md) for the full usage guide. ### Payment Connector -| Field | Required | Description | -| ---------------- | -------- | -------------------------------------------------- | -| `name` | Yes | Connector name (alphanumeric + underscore, max 48) | -| `provider` | No | `"CoinbaseCDP"` (default) or `"StripePrivy"` | -| `credentialName` | Yes | Name of the credential (maps to `.env.local` vars) | +| Field | Required | Description | +| ---------------- | -------- | ----------------------------------------------------------------- | +| `name` | Yes | Connector name (alphanumeric + underscore, max 48) | +| `provider` | Yes | `"CoinbaseCDP"` for Quick Create; either provider for manual mode | +| `provisionMode` | Cond. | `"QUICK_CREATE"` for Quick Create; omit or use `"MANUAL"` | +| `credentialName` | Cond. | Required for manual mode; forbidden for Quick Create | ### Payment Credential Provider -Payment connectors use a `PaymentCredentialProvider` credential type, distinct from `ApiKeyCredentialProvider` and -`OAuthCredentialProvider`. The credential is automatically created during `agentcore deploy` from values in -`.env.local`. You do not need to add it to the `credentials` array manually. +Manual payment connectors use a `PaymentCredentialProvider` credential type, distinct from `ApiKeyCredentialProvider` +and `OAuthCredentialProvider`. The credential is automatically created during `agentcore deploy` from values in +`.env.local`. Quick Create provisions its provider through the service and does not add a local credential entry. --- diff --git a/docs/payments.md b/docs/payments.md index ddb274d45..32b58e8de 100644 --- a/docs/payments.md +++ b/docs/payments.md @@ -18,16 +18,13 @@ cd MyProject # 2. Add a payment manager agentcore add payment-manager --name MyManager -# 3. Add a payment connector with CoinbaseCDP credentials +# 3. Add a Coinbase connector with Quick Create agentcore add payment-connector \ --manager MyManager \ --name MyCDPConnector \ - --provider CoinbaseCDP \ - --api-key-id your-api-key-id \ - --api-key-secret your-api-key-secret \ - --wallet-secret your-wallet-secret + --provision-mode QUICK_CREATE -# 4. Deploy (creates payment infrastructure on AWS) +# 4. Deploy, then open the authorization URL printed by the CLI agentcore deploy -y # 5. Create + fund an instrument out-of-band (SDK), then invoke with auto-session @@ -126,8 +123,28 @@ For details on IAM role separation (ManagementRole vs ProcessPaymentRole), see A payment connector links a credential provider (wallet credentials) to a payment manager. Each manager needs at least one connector before it can process payments. +### Quick Create with Coinbase (Recommended) + +Quick Create provisions the Coinbase credential provider through AWS after deployment. It does not collect credentials, +add a local credential entry, or write payment variables to `.env.local`. + +```bash +agentcore add payment-connector \ + --manager MyManager \ + --name MyCDPConnector \ + --provision-mode QUICK_CREATE + +agentcore deploy -y +``` + +Deploy prints the live authorization URL. Open it to complete consent. The deployment succeeds while the connector is +`PENDING_AUTHENTICATION`; use `agentcore status --type payment` to retrieve the current status and URL. Once consent +completes, status becomes `READY` and the generated credential provider ARN appears in status output. + ### CoinbaseCDP Provider +Manual mode remains available when you already manage Coinbase CDP credentials. + ```bash agentcore add payment-connector \ --manager MyManager \ @@ -174,8 +191,8 @@ agentcore add payment-connector \ ### Credential Storage -Connector credentials are stored in `agentcore/.env.local` and never committed to source control. The env var naming -convention is: +Manual connector credentials are stored in `agentcore/.env.local` and never committed to source control. Quick Create +does not use local payment credentials. The manual env var naming convention is: **CoinbaseCDP** (3 variables): @@ -462,11 +479,12 @@ agentcore remove payment-manager --name MyManager -y ``` Removing a payment manager cascades: it deletes all associated connectors and credential providers from the local -configuration. +configuration. The CLI never imperatively deletes a provider generated by Quick Create. ## Validation -`agentcore validate` checks payment configuration for common issues: +`agentcore validate` checks payment configuration for common issues. Credential and `.env.local` checks apply only to +manual connectors: - Credential cross-references: verifies each connector's `credentialName` maps to a valid credential entry - `.env.local` existence: confirms the secrets file exists when payment connectors are configured diff --git a/docs/policies/iam-policy-user.json b/docs/policies/iam-policy-user.json index b9100d39e..4436d3613 100644 --- a/docs/policies/iam-policy-user.json +++ b/docs/policies/iam-policy-user.json @@ -102,6 +102,7 @@ "bedrock-agentcore:UpdatePaymentCredentialProvider", "bedrock-agentcore:DeletePaymentCredentialProvider", "bedrock-agentcore:GetPaymentManager", + "bedrock-agentcore:GetPaymentConnector", "bedrock-agentcore:ListPaymentSessions", "bedrock-agentcore:CreatePaymentSession" ], diff --git a/integ-tests/add-remove-payment.test.ts b/integ-tests/add-remove-payment.test.ts index 21d80bb21..a0e4834bf 100644 --- a/integ-tests/add-remove-payment.test.ts +++ b/integ-tests/add-remove-payment.test.ts @@ -317,6 +317,125 @@ describe('integration: add and remove payment managers and connectors', () => { }); }); + describe('Quick Create connector lifecycle', () => { + const managerName = `IntegQuickMgr${Date.now().toString().slice(-6)}`; + const connectorName = `IntegQuick${Date.now().toString().slice(-6)}`; + let envBefore = ''; + + beforeAll(async () => { + await runCLI(['add', 'payment-manager', '--name', managerName], project.projectPath); + envBefore = await readFile(join(project.projectPath, 'agentcore', '.env.local'), 'utf-8').catch(() => ''); + }); + + it('adds Quick Create without provider or credential flags', async () => { + const result = await runCLI( + [ + 'add', + 'payment-connector', + '--manager', + managerName, + '--name', + connectorName, + '--provision-mode', + 'QUICK_CREATE', + '--json', + ], + project.projectPath + ); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + expect(JSON.parse(result.stdout)).toEqual( + expect.objectContaining({ + success: true, + managerName, + connectorName, + }) + ); + + const config = await readProjectConfig(project.projectPath); + const manager = config.payments?.find((p: Record) => p.name === managerName); + expect(manager?.connectors).toEqual([ + { + name: connectorName, + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ]); + expect( + config.credentials?.some((c: Record) => c.authorizerType === 'PaymentCredentialProvider') + ).toBe(false); + }); + + it('does not write payment secrets to .env.local', async () => { + const envAfter = await readFile(join(project.projectPath, 'agentcore', '.env.local'), 'utf-8').catch(() => ''); + expect(envAfter).toBe(envBefore); + }); + + it('rejects credential flags with Quick Create', async () => { + const result = await runCLI( + [ + 'add', + 'payment-connector', + '--manager', + managerName, + '--name', + `${connectorName}Secret`, + '--provision-mode', + 'QUICK_CREATE', + '--api-key-id', + 'must-not-be-used', + '--json', + ], + project.projectPath + ); + + expect(result.exitCode).toBe(1); + expect(JSON.parse(result.stdout).error).toContain('Credential options cannot be used with QUICK_CREATE'); + }); + + it('rejects StripePrivy with Quick Create', async () => { + const result = await runCLI( + [ + 'add', + 'payment-connector', + '--manager', + managerName, + '--name', + `${connectorName}Stripe`, + '--provision-mode', + 'QUICK_CREATE', + '--provider', + 'StripePrivy', + '--json', + ], + project.projectPath + ); + + expect(result.exitCode).toBe(1); + expect(JSON.parse(result.stdout).error).toContain('QUICK_CREATE only supports the CoinbaseCDP provider'); + }); + + it('validates a Quick Create connector without local credentials', async () => { + const result = await runCLI(['validate'], project.projectPath); + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + }); + + it('removes Quick Create without credential cleanup', async () => { + const result = await runCLI( + ['remove', 'payment-connector', '--manager', managerName, '--name', connectorName, '--yes', '--json'], + project.projectPath + ); + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + + const envAfter = await readFile(join(project.projectPath, 'agentcore', '.env.local'), 'utf-8').catch(() => ''); + expect(envAfter).toBe(envBefore); + }); + + afterAll(async () => { + await runCLI(['remove', 'payment-manager', '--name', managerName, '--yes'], project.projectPath); + }); + }); + describe('StripePrivy connector lifecycle', () => { const managerName = `IntegSpMgr${Date.now().toString().slice(-6)}`; const connectorName = `IntegSpConn${Date.now().toString().slice(-6)}`; diff --git a/integ-tests/tui/add-payment-quick-create.test.ts b/integ-tests/tui/add-payment-quick-create.test.ts new file mode 100644 index 000000000..c8c49afec --- /dev/null +++ b/integ-tests/tui/add-payment-quick-create.test.ts @@ -0,0 +1,106 @@ +import { TuiSession, WaitForTimeoutError } from '../../src/tui-harness/index.js'; +import { createMinimalProjectDir } from './helpers.js'; +import type { MinimalProjectDirResult } from './helpers.js'; +import { readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const CLI_DIST = join(__dirname, '..', '..', 'dist', 'cli', 'index.mjs'); + +function screenText(session: TuiSession): string { + return session.readScreen().lines.join('\n'); +} + +async function waitFor(session: TuiSession, pattern: string | RegExp, timeoutMs = 10_000): Promise { + try { + await session.waitFor(pattern, timeoutMs); + return true; + } catch (error) { + if (error instanceof WaitForTimeoutError) return false; + throw error; + } +} + +describe('Add Payment Connector Quick Create Flow', () => { + let session: TuiSession; + let project: MinimalProjectDirResult; + let configPath: string; + + beforeAll(async () => { + project = await createMinimalProjectDir({ projectName: 'QuickCreateTui' }); + configPath = join(project.dir, 'agentcore', 'agentcore.json'); + const config = JSON.parse(await readFile(configPath, 'utf-8')); + config.payments = [ + { + name: 'TuiManager', + authorizerType: 'AWS_IAM', + connectors: [], + autoPayment: true, + defaultSpendLimit: '10.00', + }, + ]; + await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8'); + + session = await TuiSession.launch({ + command: process.execPath, + args: [CLI_DIST, 'add', 'payment-connector'], + cwd: project.dir, + cols: 120, + rows: 35, + }); + }); + + afterAll(async () => { + if (session?.alive) await session.close(); + if (project) await project.cleanup(); + }); + + it('auto-selects the only manager and lists Quick Create first', async () => { + const found = await waitFor(session, 'Choose connector setup', 15_000); + expect(found, screenText(session)).toBe(true); + const text = screenText(session); + expect(text).toContain('Quick Create with Coinbase'); + expect(text.indexOf('Quick Create with Coinbase')).toBeLessThan(text.indexOf('Coinbase CDP credentials')); + expect(text.indexOf('Coinbase CDP credentials')).toBeLessThan(text.indexOf('Stripe + Privy credentials')); + expect(text).not.toContain('Select payment manager'); + }); + + it('skips credential entry and collects the connector name', async () => { + await session.sendSpecialKey('enter'); + expect(await waitFor(session, 'Connector name')).toBe(true); + const text = screenText(session); + expect(text).not.toContain('API Key ID'); + expect(text).not.toContain('Wallet Secret'); + expect(text).not.toContain('Privy App ID'); + }); + + it('shows a secret-free Quick Create confirmation', async () => { + await session.sendSpecialKey('enter'); + expect(await waitFor(session, 'Review Configuration')).toBe(true); + const text = screenText(session); + expect(text).toContain('Manager: TuiManager'); + expect(text).toContain('Provisioning: Quick Create'); + expect(text).toContain('Provider: Coinbase CDP'); + expect(text).not.toContain('API Key'); + expect(text).not.toContain('Wallet Secret'); + expect(text).not.toContain('App Secret'); + }); + + it('persists the Quick Create shape and explains the deploy handoff', async () => { + await session.sendSpecialKey('enter'); + expect(await waitFor(session, 'receive its authorization URL')).toBe(true); + + const config = JSON.parse(await readFile(configPath, 'utf-8')); + expect(config.credentials).toEqual([]); + expect(config.payments[0].connectors).toEqual([ + { + name: 'MyCdpConnector', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ]); + }); +}); diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 857f0dbb3..01be3db50 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -186,7 +186,12 @@ async function main() { autoPayment?: boolean; paymentToolAllowlist?: string[]; networkPreferences?: string[]; - connectors: { name: string; provider?: string; credentialName: string }[]; + connectors: { + name: string; + provider?: string; + provisionMode?: 'MANUAL' | 'QUICK_CREATE'; + credentialName?: string; + }[]; }) => ({ name: p.name, description: p.description, @@ -196,6 +201,19 @@ async function main() { paymentToolAllowlist: p.paymentToolAllowlist, networkPreferences: p.networkPreferences, connectors: p.connectors.map(c => { + if (c.provisionMode === 'QUICK_CREATE') { + return { + name: c.name, + provider: 'CoinbaseCDP' as const, + provisionMode: 'QUICK_CREATE' as const, + }; + } + + if (!c.credentialName) { + throw new Error( + \`Manual payment connector "\${c.name}" on manager "\${p.name}" is missing its credential name.\` + ); + } const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn; if (!credentialProviderArn) { // Fail fast with an actionable message rather than passing an empty @@ -206,7 +224,13 @@ async function main() { \`Run \\\`agentcore deploy\\\` so the credential provider is created first.\` ); } - return { name: c.name, provider: c.provider, credentialProviderArn }; + return { + name: c.name, + provider: c.provider, + ...(c.provisionMode && { provisionMode: c.provisionMode }), + credentialName: c.credentialName, + credentialProviderArn, + }; }), }) ) @@ -378,12 +402,24 @@ import { Construct } from 'constructs'; */ export type HarnessConfig = HarnessDeploymentConfig; -export interface PaymentConnectorSpec { +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; @@ -562,13 +598,31 @@ export class AgentCoreStack extends Stack { // Create connectors for this manager for (const connector of payment.connectors) { const connId = toCdkId(connector.name); - const conn = new AgentCorePaymentConnector(this, \`Payment\${mgrId}\${connId}\`, { + 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, - credentialProviderArn: connector.credentialProviderArn, - }); + ...(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]) { @@ -581,6 +635,18 @@ export class AgentCoreStack extends Stack { 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 diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index b15b3281d..4349182fa 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -141,7 +141,12 @@ async function main() { autoPayment?: boolean; paymentToolAllowlist?: string[]; networkPreferences?: string[]; - connectors: { name: string; provider?: string; credentialName: string }[]; + connectors: { + name: string; + provider?: string; + provisionMode?: 'MANUAL' | 'QUICK_CREATE'; + credentialName?: string; + }[]; }) => ({ name: p.name, description: p.description, @@ -151,6 +156,19 @@ async function main() { paymentToolAllowlist: p.paymentToolAllowlist, networkPreferences: p.networkPreferences, connectors: p.connectors.map(c => { + if (c.provisionMode === 'QUICK_CREATE') { + return { + name: c.name, + provider: 'CoinbaseCDP' as const, + provisionMode: 'QUICK_CREATE' as const, + }; + } + + if (!c.credentialName) { + throw new Error( + `Manual payment connector "${c.name}" on manager "${p.name}" is missing its credential name.` + ); + } const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn; if (!credentialProviderArn) { // Fail fast with an actionable message rather than passing an empty @@ -161,7 +179,13 @@ async function main() { `Run \`agentcore deploy\` so the credential provider is created first.` ); } - return { name: c.name, provider: c.provider, credentialProviderArn }; + return { + name: c.name, + provider: c.provider, + ...(c.provisionMode && { provisionMode: c.provisionMode }), + credentialName: c.credentialName, + credentialProviderArn, + }; }), }) ) diff --git a/src/assets/cdk/lib/cdk-stack.ts b/src/assets/cdk/lib/cdk-stack.ts index 3dac0669d..19ba23930 100644 --- a/src/assets/cdk/lib/cdk-stack.ts +++ b/src/assets/cdk/lib/cdk-stack.ts @@ -19,12 +19,24 @@ import { Construct } from 'constructs'; */ export type HarnessConfig = HarnessDeploymentConfig; -export interface PaymentConnectorSpec { +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; @@ -203,13 +215,31 @@ export class AgentCoreStack extends Stack { // Create connectors for this manager for (const connector of payment.connectors) { const connId = toCdkId(connector.name); - const conn = new AgentCorePaymentConnector(this, `Payment${mgrId}${connId}`, { + 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, - credentialProviderArn: connector.credentialProviderArn, - }); + ...(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]) { @@ -222,6 +252,18 @@ export class AgentCoreStack extends Stack { 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 diff --git a/src/cli/aws/__tests__/agentcore-payments-get-connector.test.ts b/src/cli/aws/__tests__/agentcore-payments-get-connector.test.ts new file mode 100644 index 000000000..8865151c3 --- /dev/null +++ b/src/cli/aws/__tests__/agentcore-payments-get-connector.test.ts @@ -0,0 +1,84 @@ +import { getPaymentConnector } from '../agentcore-payments'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockSign, mockFetch } = vi.hoisted(() => ({ + mockSign: vi.fn(request => Promise.resolve(request)), + mockFetch: vi.fn(), +})); + +vi.mock('../account', () => ({ + getCredentialProvider: () => () => + Promise.resolve({ + accessKeyId: 'test', + secretAccessKey: 'test', + }), +})); + +vi.mock('../stage-endpoint', () => ({ + controlPlaneEndpoint: (region: string) => `https://bedrock-agentcore-control.${region}.amazonaws.com`, + dataPlaneEndpoint: (region: string) => `https://bedrock-agentcore.${region}.amazonaws.com`, +})); + +vi.mock('@smithy/signature-v4', () => ({ + SignatureV4: class { + sign = mockSign; + }, +})); + +describe('getPaymentConnector', () => { + beforeEach(() => { + vi.stubGlobal('fetch', mockFetch); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); + }); + + it('uses the GetPaymentConnector REST path and returns live authorization state', async () => { + const response = { + paymentConnectorId: 'connector-123', + name: 'Quick', + type: 'CoinbaseCDP', + credentialProviderConfigurations: [], + createdAt: '2026-08-17T00:00:00Z', + lastUpdatedAt: '2026-08-17T00:00:00Z', + status: 'PENDING_AUTHENTICATION', + authorizationUrl: 'https://example.com/authorize', + }; + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(response), + }); + + await expect( + getPaymentConnector({ + region: 'ap-southeast-2', + paymentManagerId: 'manager/123', + paymentConnectorId: 'connector 123', + }) + ).resolves.toEqual(response); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://bedrock-agentcore-control.ap-southeast-2.amazonaws.com/payments/managers/manager%2F123/connectors/connector%20123', + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('returns null for a missing connector', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + text: () => Promise.resolve(JSON.stringify({ code: 'ResourceNotFoundException', message: 'not found' })), + }); + + await expect( + getPaymentConnector({ + region: 'ap-southeast-2', + paymentManagerId: 'manager-123', + paymentConnectorId: 'missing', + }) + ).resolves.toBeNull(); + }); +}); diff --git a/src/cli/aws/agentcore-payments.ts b/src/cli/aws/agentcore-payments.ts index 7e8f5fc48..74dd4acce 100644 --- a/src/cli/aws/agentcore-payments.ts +++ b/src/cli/aws/agentcore-payments.ts @@ -78,6 +78,45 @@ interface PaymentManagerDetail { roleArn?: string; } +// ── Get Payment Connector ───────────────────────────────────────────────── + +export interface GetPaymentConnectorOptions { + region: string; + paymentManagerId: string; + paymentConnectorId: string; +} + +export type PaymentConnectorStatus = + | 'CREATING' + | 'UPDATING' + | 'DELETING' + | 'READY' + | 'CREATE_FAILED' + | 'UPDATE_FAILED' + | 'DELETE_FAILED' + | 'AWS_MARKETPLACE_SUBSCRIPTION_REQUIRED' + | 'PENDING_AUTHENTICATION' + | 'PROVISIONING' + | 'AUTHENTICATION_EXPIRED' + | 'AUTHENTICATION_FAILED'; + +export interface PaymentCredentialProviderConfiguration { + coinbaseCDP?: { credentialProviderArn: string }; + stripePrivy?: { credentialProviderArn: string }; +} + +export interface PaymentConnectorDetail { + paymentConnectorId: string; + name: string; + description?: string; + type: 'CoinbaseCDP' | 'StripePrivy'; + credentialProviderConfigurations: PaymentCredentialProviderConfiguration[]; + createdAt: number | string; + lastUpdatedAt: number | string; + status: PaymentConnectorStatus; + authorizationUrl?: string; +} + // ============================================================================ // HTTP signing helper // ============================================================================ @@ -359,6 +398,23 @@ export async function getPaymentManager(options: GetPaymentManagerOptions): Prom } } +export async function getPaymentConnector(options: GetPaymentConnectorOptions): Promise { + try { + return (await signedRequest({ + region: options.region, + method: 'GET', + path: + `/payments/managers/${encodeURIComponent(options.paymentManagerId)}/connectors/` + + encodeURIComponent(options.paymentConnectorId), + })) as PaymentConnectorDetail; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const code = (err as { code?: unknown }).code; + if (code === 'ResourceNotFoundException' || msg.includes('(404)')) return null; + throw rethrowWithContext(`Failed to get payment connector "${options.paymentConnectorId}"`, err); + } +} + // ============================================================================ // Data Plane Operations (Payment Sessions) // ============================================================================ diff --git a/src/cli/cloudformation/__tests__/parse-payment-outputs.test.ts b/src/cli/cloudformation/__tests__/parse-payment-outputs.test.ts index 3a49ebe0d..788f6c5b9 100644 --- a/src/cli/cloudformation/__tests__/parse-payment-outputs.test.ts +++ b/src/cli/cloudformation/__tests__/parse-payment-outputs.test.ts @@ -1,4 +1,4 @@ -import { parsePaymentOutputs } from '../outputs.js'; +import { omitPaymentAuthorizationOutputs, parsePaymentOutputs } from '../outputs.js'; import type { StackOutputs } from '../outputs.js'; import { describe, expect, it } from 'vitest'; @@ -60,6 +60,44 @@ describe('parsePaymentOutputs', () => { credentialProviderName: 'coinbase-cdp', }); }); + + it('stores a Quick Create connector without credential provider state', () => { + const outputs: StackOutputs = { + ...makeOutputs('MyManager'), + PaymentMyManagerQuickConnectorId: 'conn-quick-001', + PaymentMyManagerQuickConnectorStatus: 'PENDING_AUTHENTICATION', + PaymentMyManagerQuickAuthorizationUrl: 'https://example.com/authorize', + }; + + const result = parsePaymentOutputs(outputs, [ + { + name: 'MyManager', + connectors: [{ name: 'Quick', provisionMode: 'QUICK_CREATE' }], + }, + ]); + + expect(result.MyManager!.connectors.Quick).toEqual({ + connectorId: 'conn-quick-001', + provisionMode: 'QUICK_CREATE', + }); + expect(JSON.stringify(result)).not.toContain('example.com/authorize'); + }); + + it('does not store an invalid manual connector without a credential provider ARN', () => { + const outputs: StackOutputs = { + ...makeOutputs('MyManager'), + PaymentMyManagerManualConnectorId: 'conn-manual-001', + }; + + const result = parsePaymentOutputs(outputs, [ + { + name: 'MyManager', + connectors: [{ name: 'Manual', provisionMode: 'MANUAL' }], + }, + ]); + + expect(result.MyManager!.connectors).toEqual({}); + }); }); describe('missing required manager fields', () => { @@ -348,3 +386,18 @@ describe('parsePaymentOutputs', () => { }); }); }); + +describe('omitPaymentAuthorizationOutputs', () => { + it('removes stale payment authorization URLs but preserves other outputs', () => { + expect( + omitPaymentAuthorizationOutputs({ + PaymentManagerConnectorAuthorizationUrl: 'https://stale.example.com', + PaymentManagerConnectorConnectorId: 'connector-123', + GatewayAuthorizationUrl: 'https://gateway.example.com', + }) + ).toEqual({ + PaymentManagerConnectorConnectorId: 'connector-123', + GatewayAuthorizationUrl: 'https://gateway.example.com', + }); + }); +}); diff --git a/src/cli/cloudformation/outputs.ts b/src/cli/cloudformation/outputs.ts index d8463a023..1fa00d9ee 100644 --- a/src/cli/cloudformation/outputs.ts +++ b/src/cli/cloudformation/outputs.ts @@ -8,6 +8,7 @@ import type { KnowledgeBaseDeployedState, MemoryDeployedState, OnlineEvalDeployedState, + PaymentConnectorDeployedState, PaymentDeployedState, PolicyDeployedState, PolicyEngineDeployedState, @@ -21,6 +22,12 @@ import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-clo export type StackOutputs = Record; +export function omitPaymentAuthorizationOutputs(outputs: StackOutputs): StackOutputs { + return Object.fromEntries( + Object.entries(outputs).filter(([key]) => !key.startsWith('Payment') || !key.endsWith('AuthorizationUrl')) + ); +} + /** * Fetch CloudFormation stack outputs. */ @@ -655,7 +662,12 @@ export function parsePaymentOutputs( autoPayment?: boolean; paymentToolAllowlist?: string[]; networkPreferences?: string[]; - connectors: { name: string; credentialProviderArn: string; credentialProviderName?: string }[]; + connectors: { + name: string; + provisionMode?: 'MANUAL' | 'QUICK_CREATE'; + credentialProviderArn?: string; + credentialProviderName?: string; + }[]; }[] ): Record { const payments: Record = {}; @@ -669,19 +681,25 @@ export function parsePaymentOutputs( if (!managerArn || !managerId || !processPaymentRoleArn || !resourceRetrievalRoleArn) continue; - const connectors: Record< - string, - { connectorId: string; credentialProviderArn: string; credentialProviderName?: string } - > = {}; + const connectors: Record = {}; for (const conn of spec.connectors) { const connId = toPaymentCdkId(conn.name); const connectorId = outputs[`Payment${mgrId}${connId}ConnectorId`]; if (connectorId) { - connectors[conn.name] = { - connectorId, - credentialProviderArn: conn.credentialProviderArn, - credentialProviderName: conn.credentialProviderName, - }; + if (conn.provisionMode === 'QUICK_CREATE') { + connectors[conn.name] = { + connectorId, + provisionMode: 'QUICK_CREATE', + ...(conn.credentialProviderName && { credentialProviderName: conn.credentialProviderName }), + }; + } else if (conn.credentialProviderArn) { + connectors[conn.name] = { + connectorId, + ...(conn.provisionMode && { provisionMode: conn.provisionMode }), + credentialProviderArn: conn.credentialProviderArn, + ...(conn.credentialProviderName && { credentialProviderName: conn.credentialProviderName }), + }; + } } } diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 437f26898..571bbf64b 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -16,6 +16,7 @@ import type { DeployMessage, SwitchableIoHost } from '../../cdk/toolkit-lib'; import { buildDeployedState, getStackOutputs, + omitPaymentAuthorizationOutputs, parseAgentOutputs, parseConfigBundleOutputs, parseDatasetOutputs, @@ -44,7 +45,9 @@ import { ensureDefaultDeploymentTarget, ensureManagedDependencies, failedSyncResult, + formatQuickCreateConnectorAuthorization, getAllCredentials, + getQuickCreateConnectorAuthorizations, hasIdentityApiProviders, hasIdentityOAuthProviders, hasManagedMemoryHarness, @@ -746,11 +749,17 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise ({ - name: c.name, - credentialProviderArn: deployedCredentials[c.credentialName]?.credentialProviderArn ?? '', - credentialProviderName: c.credentialName, - })), + connectors: p.connectors.map(c => { + if (c.provisionMode === 'QUICK_CREATE') { + return { name: c.name, provisionMode: 'QUICK_CREATE' as const }; + } + return { + name: c.name, + ...(c.provisionMode && { provisionMode: c.provisionMode }), + credentialProviderArn: deployedCredentials[c.credentialName]?.credentialProviderArn, + credentialProviderName: c.credentialName, + }; + }), })); const payments = paymentSpecs.length > 0 ? parsePaymentOutputs(outputs, paymentSpecs) : undefined; @@ -978,6 +987,12 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise 0 || hasHarnesses; const nextSteps = hasInvokable ? [...AGENT_NEXT_STEPS] : [...MEMORY_ONLY_NEXT_STEPS]; const notes: string[] = [...autoPaymentNotices]; + const quickCreateAuthorizations = await getQuickCreateConnectorAuthorizations({ + region: target.region, + projectSpec: context.projectSpec, + payments, + }); + notes.push(...quickCreateAuthorizations.map(formatQuickCreateConnectorAuthorization)); const hasPythonAgent = context.projectSpec.runtimes?.some(a => a.entrypoint?.endsWith('.py') || a.entrypoint?.includes('.py:')) ?? false; if ((agentNames.length > 0 || hasGateways) && hasPythonAgent) { @@ -1014,7 +1029,7 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise const current = await configIO.readProjectSpec(); for (const payment of current.payments ?? []) { for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; const provider = connector.provider ?? 'CoinbaseCDP'; if (provider === 'StripePrivy') { const vars = computeStripePrivyCredentialEnvVarNames(connector.credentialName); diff --git a/src/cli/commands/status/__tests__/action.test.ts b/src/cli/commands/status/__tests__/action.test.ts index faceb2812..d12f57af3 100644 --- a/src/cli/commands/status/__tests__/action.test.ts +++ b/src/cli/commands/status/__tests__/action.test.ts @@ -10,6 +10,8 @@ const mockGetEvaluator = vi.fn(); const mockGetOnlineEvaluationConfig = vi.fn(); const mockGetKnowledgeBase = vi.fn(); const mockGetLatestIngestionJob = vi.fn(); +const mockGetPaymentManager = vi.fn(); +const mockGetPaymentConnector = vi.fn(); vi.mock('../../../aws', () => ({ getAgentRuntimeStatus: (...args: unknown[]) => mockGetAgentRuntimeStatus(...args), @@ -25,6 +27,11 @@ vi.mock('../../../aws/bedrock-agent', () => ({ getLatestIngestionJob: (...args: unknown[]) => mockGetLatestIngestionJob(...args), })); +vi.mock('../../../aws/agentcore-payments', () => ({ + getPaymentManager: (...args: unknown[]) => mockGetPaymentManager(...args), + getPaymentConnector: (...args: unknown[]) => mockGetPaymentConnector(...args), +})); + const loggedLines: string[] = []; vi.mock('../../../logging', () => { return { @@ -754,6 +761,8 @@ describe('handleProjectStatus — live enrichment', () => { mockGetAgentRuntimeStatus.mockReset(); mockGetEvaluator.mockReset(); mockGetOnlineEvaluationConfig.mockReset(); + mockGetPaymentManager.mockReset(); + mockGetPaymentConnector.mockReset(); }); afterEach(() => vi.clearAllMocks()); @@ -917,6 +926,100 @@ describe('handleProjectStatus — live enrichment', () => { expect(mockGetOnlineEvaluationConfig).not.toHaveBeenCalled(); }); + function makePaymentContext(): StatusContext { + return makeContext({ + project: { + ...baseProject, + payments: [ + { + name: 'PayManager', + authorizerType: 'AWS_IAM', + connectors: [ + { + name: 'QuickConnector', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ], + autoPayment: false, + defaultSpendLimit: '10.00', + }, + ], + } as unknown as AgentCoreProjectSpec, + deployedState: { + targets: { + dev: { + resources: { + payments: { + PayManager: { + managerId: 'manager-123', + managerArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:payment-manager/manager-123', + connectors: { + QuickConnector: { + connectorId: 'connector-123', + provisionMode: 'QUICK_CREATE', + }, + }, + processPaymentRoleArn: 'arn:aws:iam::123456789:role/process', + resourceRetrievalRoleArn: 'arn:aws:iam::123456789:role/retrieval', + }, + }, + }, + }, + }, + }, + }); + } + + it('omits a retained authorization URL when the connector is READY', async () => { + mockGetPaymentManager.mockResolvedValue({ status: 'READY' }); + mockGetPaymentConnector.mockResolvedValue({ + paymentConnectorId: 'connector-123', + name: 'QuickConnector', + type: 'CoinbaseCDP', + status: 'READY', + authorizationUrl: 'https://stale.example.com/authorize', + credentialProviderConfigurations: [ + { + coinbaseCDP: { + credentialProviderArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:payment-provider/generated', + }, + }, + ], + }); + + const result = await handleProjectStatus(makePaymentContext()); + + assert(result.success); + const payment = result.resources.find(resource => resource.resourceType === 'payment'); + expect(payment?.paymentConnectors).toEqual([ + { + name: 'QuickConnector', + connectorId: 'connector-123', + status: 'READY', + credentialProviderArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:payment-provider/generated', + }, + ]); + }); + + it('includes the authorization URL while the connector is PENDING_AUTHENTICATION', async () => { + mockGetPaymentManager.mockResolvedValue({ status: 'READY' }); + mockGetPaymentConnector.mockResolvedValue({ + paymentConnectorId: 'connector-123', + name: 'QuickConnector', + type: 'CoinbaseCDP', + status: 'PENDING_AUTHENTICATION', + authorizationUrl: 'https://current.example.com/authorize', + credentialProviderConfigurations: [], + }); + + const result = await handleProjectStatus(makePaymentContext()); + + assert(result.success); + const payment = result.resources.find(resource => resource.resourceType === 'payment'); + expect(payment?.paymentConnectors?.[0]?.authorizationUrl).toBe('https://current.example.com/authorize'); + }); + it('does not enrich local-only evaluators', async () => { const ctx = makeContext({ deployedState: { diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index 12bccd24d..c3462d9d4 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -3,7 +3,7 @@ import type { Result } from '../../../lib/result'; import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedResourceState, DeployedState } from '../../../schema'; import { getAgentRuntimeStatus } from '../../aws'; import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-control'; -import { getPaymentManager } from '../../aws/agentcore-payments'; +import { getPaymentConnector, getPaymentManager } from '../../aws/agentcore-payments'; import { getKnowledgeBase, getLatestIngestionJob } from '../../aws/bedrock-agent'; import { getErrorMessage } from '../../errors'; import { ExecLogger } from '../../logging'; @@ -18,6 +18,15 @@ import { export type { ResourceDeploymentState }; +export interface PaymentConnectorStatusEntry { + name: string; + connectorId: string; + status: string; + authorizationUrl?: string; + credentialProviderArn?: string; + error?: string; +} + export interface ResourceStatusEntry { resourceType: | 'agent' @@ -41,6 +50,7 @@ export interface ResourceStatusEntry { parentName?: string; error?: string; invocationUrl?: string; + paymentConnectors?: PaymentConnectorStatusEntry[]; } export type ProjectStatusResult = Result<{ @@ -714,6 +724,58 @@ export async function handleProjectStatus( resources[i] = { ...entry, detail: `unknown — ${connectorCount} connector(s)`, error: errorMsg }; logger.log(` ${entry.name}: unknown (fetch failed) - ${errorMsg}`, 'error'); } + + const paymentConnectors = await Promise.all( + Object.entries(paymentState.connectors ?? {}).map( + async ([connectorName, connectorState]): Promise => { + try { + const detail = await getPaymentConnector({ + region: targetConfig.region, + paymentManagerId: paymentState.managerId, + paymentConnectorId: connectorState.connectorId, + }); + if (!detail) { + return { + name: connectorName, + connectorId: connectorState.connectorId, + status: 'NOT_FOUND', + error: 'Connector was not found by the Payments service', + }; + } + const credentialProviderArn = detail.credentialProviderConfigurations + .map( + configuration => + configuration.coinbaseCDP?.credentialProviderArn ?? + configuration.stripePrivy?.credentialProviderArn + ) + .find((arn): arn is string => Boolean(arn)); + return { + name: connectorName, + connectorId: connectorState.connectorId, + status: detail.status, + ...(detail.status === 'PENDING_AUTHENTICATION' && + detail.authorizationUrl && { authorizationUrl: detail.authorizationUrl }), + ...(credentialProviderArn && { credentialProviderArn }), + }; + } catch (error) { + return { + name: connectorName, + connectorId: connectorState.connectorId, + status: 'UNKNOWN', + error: getErrorMessage(error), + }; + } + } + ) + ); + resources[i] = { ...resources[i], paymentConnectors }; + for (const connector of paymentConnectors) { + logger.log( + ` ${connector.name}: ${connector.status} (${connector.connectorId})` + + (connector.error ? ` - ${connector.error}` : ''), + connector.error ? 'error' : undefined + ); + } }) ); diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index bafccb023..568ef76d0 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -392,9 +392,29 @@ export const registerStatus = (program: Command) => { {payments.length > 0 && ( Payments - {payments.map(entry => ( - - ))} + {payments.map(entry => { + return ( + + + {entry.paymentConnectors?.map(connector => ( + + + {connector.name}: {connector.status} ({connector.connectorId}) + + {connector.authorizationUrl && ( + + Authorization: {connector.authorizationUrl} + + )} + {connector.credentialProviderArn && ( + Credential provider: {connector.credentialProviderArn} + )} + {connector.error && {connector.error}} + + ))} + + ); + })} )} diff --git a/src/cli/commands/validate/action.ts b/src/cli/commands/validate/action.ts index 57ed7252b..ce5bb1844 100644 --- a/src/cli/commands/validate/action.ts +++ b/src/cli/commands/validate/action.ts @@ -60,6 +60,7 @@ export async function handleValidate(options: ValidateOptions): Promise }; } for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; const credential = projectSpec.credentials?.find(c => c.name === connector.credentialName); if (!credential) { return { @@ -91,12 +92,13 @@ export async function handleValidate(options: ValidateOptions): Promise } // Check .env.local has required variables - const hasConnectors = projectSpec.payments.some(p => p.connectors.length > 0); + const hasConnectors = projectSpec.payments.some(p => p.connectors.some(c => c.provisionMode !== 'QUICK_CREATE')); const envFilePath = join(configRoot, '.env.local'); if (hasConnectors && !existsSync(envFilePath)) { const expectedVars: string[] = []; for (const payment of projectSpec.payments) { for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; const provider = connector.provider ?? 'CoinbaseCDP'; if (provider === 'StripePrivy') { const vars = computeStripePrivyCredentialEnvVarNames(connector.credentialName); @@ -120,6 +122,7 @@ export async function handleValidate(options: ValidateOptions): Promise const credentials = SecureCredentials.fromEnvVars(envVars); for (const payment of projectSpec.payments) { for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; const provider = connector.provider ?? 'CoinbaseCDP'; if (provider === 'StripePrivy') { const vars = computeStripePrivyCredentialEnvVarNames(connector.credentialName); diff --git a/src/cli/operations/deploy/__tests__/assert-env-file.test.ts b/src/cli/operations/deploy/__tests__/assert-env-file.test.ts index a1d66ff55..def8e052a 100644 --- a/src/cli/operations/deploy/__tests__/assert-env-file.test.ts +++ b/src/cli/operations/deploy/__tests__/assert-env-file.test.ts @@ -104,6 +104,8 @@ describe('assertEnvFileExists', () => { { name: 'PayMgr', authorizerType: 'AWS_IAM', + autoPayment: true, + defaultSpendLimit: '10.00', connectors: [ { name: 'stripeconn', provider: 'StripePrivy', credentialName: 'PayMgr-stripeconn-stripe-privy' }, ], @@ -120,6 +122,30 @@ describe('assertEnvFileExists', () => { } }); + it('does not require an env file for a Quick Create connector', () => { + mockExistsSync.mockReturnValue(false); + const spec = makeSpec({ + payments: [ + { + name: 'PayMgr', + authorizerType: 'AWS_IAM', + autoPayment: true, + defaultSpendLimit: '10.00', + connectors: [ + { + name: 'quickconn', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ], + }, + ], + }); + + expect(assertEnvFileExists(spec, BASE_DIR).success).toBe(true); + expect(mockExistsSync).not.toHaveBeenCalled(); + }); + it('combines all credential types in a single error', () => { mockExistsSync.mockReturnValue(false); const spec = makeSpec({ @@ -164,4 +190,26 @@ describe('getAllCredentials', () => { expect(result.length).toBe(3); expect(result.every(c => c.providerName === 'PayMgr-cdpconn-cdp')).toBe(true); }); + + it('skips Quick Create connectors', () => { + const spec = makeSpec({ + payments: [ + { + name: 'PayMgr', + authorizerType: 'AWS_IAM', + autoPayment: true, + defaultSpendLimit: '10.00', + connectors: [ + { + name: 'quickconn', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ], + }, + ], + }); + + expect(getAllCredentials(spec)).toEqual([]); + }); }); diff --git a/src/cli/operations/deploy/__tests__/payment-connector-authorization.test.ts b/src/cli/operations/deploy/__tests__/payment-connector-authorization.test.ts new file mode 100644 index 000000000..16d48901e --- /dev/null +++ b/src/cli/operations/deploy/__tests__/payment-connector-authorization.test.ts @@ -0,0 +1,127 @@ +import { + formatQuickCreateConnectorAuthorization, + getQuickCreateConnectorAuthorizations, +} from '../payment-connector-authorization'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockGetPaymentConnector } = vi.hoisted(() => ({ + mockGetPaymentConnector: vi.fn(), +})); + +vi.mock('../../../aws/agentcore-payments', () => ({ + getPaymentConnector: mockGetPaymentConnector, +})); + +const projectSpec = { + name: 'TestProject', + payments: [ + { + name: 'PayMgr', + authorizerType: 'AWS_IAM', + connectors: [ + { + name: 'Quick', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + { + name: 'Manual', + provider: 'CoinbaseCDP', + credentialName: 'manual-credential', + }, + ], + }, + ], + credentials: [], + runtimes: [], +} as any; + +const payments = { + PayMgr: { + managerId: 'manager-123', + managerArn: 'arn:manager', + connectors: { + Quick: { + connectorId: 'connector-123', + provisionMode: 'QUICK_CREATE' as const, + }, + }, + processPaymentRoleArn: 'arn:process-role', + resourceRetrievalRoleArn: 'arn:retrieval-role', + }, +}; + +describe('getQuickCreateConnectorAuthorizations', () => { + afterEach(() => vi.clearAllMocks()); + + it('reads only Quick Create connectors using deployed identifiers', async () => { + mockGetPaymentConnector.mockResolvedValue({ + paymentConnectorId: 'connector-123', + name: 'Quick', + type: 'CoinbaseCDP', + credentialProviderConfigurations: [], + createdAt: '2026-08-17T00:00:00Z', + lastUpdatedAt: '2026-08-17T00:00:00Z', + status: 'PENDING_AUTHENTICATION', + authorizationUrl: 'https://example.com/authorize', + }); + + const result = await getQuickCreateConnectorAuthorizations({ + region: 'ap-southeast-2', + projectSpec, + payments, + }); + + expect(mockGetPaymentConnector).toHaveBeenCalledOnce(); + expect(mockGetPaymentConnector).toHaveBeenCalledWith({ + region: 'ap-southeast-2', + paymentManagerId: 'manager-123', + paymentConnectorId: 'connector-123', + }); + expect(result).toEqual([ + { + managerName: 'PayMgr', + connectorName: 'Quick', + connectorId: 'connector-123', + status: 'PENDING_AUTHENTICATION', + authorizationUrl: 'https://example.com/authorize', + }, + ]); + }); + + it('keeps deployment successful when the live read fails', async () => { + mockGetPaymentConnector.mockRejectedValue(new Error('network unavailable')); + + const [result] = await getQuickCreateConnectorAuthorizations({ + region: 'ap-southeast-2', + projectSpec, + payments, + }); + + expect(result?.error).toBe('network unavailable'); + expect(formatQuickCreateConnectorAuthorization(result!)).toContain('Run `agentcore status` to retry'); + }); +}); + +describe('formatQuickCreateConnectorAuthorization', () => { + it('prints the live URL only while pending', () => { + expect( + formatQuickCreateConnectorAuthorization({ + managerName: 'PayMgr', + connectorName: 'Quick', + status: 'PENDING_AUTHENTICATION', + authorizationUrl: 'https://example.com/authorize', + }) + ).toContain('https://example.com/authorize'); + }); + + it('reports READY without an authorization URL', () => { + const notice = formatQuickCreateConnectorAuthorization({ + managerName: 'PayMgr', + connectorName: 'Quick', + status: 'READY', + }); + expect(notice).toContain('is ready'); + expect(notice).not.toContain('http'); + }); +}); diff --git a/src/cli/operations/deploy/__tests__/pre-deploy-payments.test.ts b/src/cli/operations/deploy/__tests__/pre-deploy-payments.test.ts index 0849dbce9..b61f82324 100644 --- a/src/cli/operations/deploy/__tests__/pre-deploy-payments.test.ts +++ b/src/cli/operations/deploy/__tests__/pre-deploy-payments.test.ts @@ -121,6 +121,26 @@ function makeStripePrivySpec(credentialName = 'my-stripe-cred') { }; } +function makeQuickCreateSpec() { + return { + name: 'test-project', + payments: [ + { + name: 'my-payment-manager', + connectors: [ + { + name: 'quick-connector', + provider: 'CoinbaseCDP' as const, + provisionMode: 'QUICK_CREATE' as const, + }, + ], + }, + ], + credentials: [], + runtimes: [], + }; +} + // ============================================================================ // setupPaymentCredentialProviders // ============================================================================ @@ -149,6 +169,24 @@ describe('setupPaymentCredentialProviders', () => { expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled(); }); + it('does not read credentials or call provider APIs for a Quick Create-only project', async () => { + const result = await setupPaymentCredentialProviders({ + projectSpec: makeQuickCreateSpec() as any, + configBaseDir: BASE_DIR, + region: REGION, + }); + + expect(result).toEqual({ + credentialProviders: {}, + hasErrors: false, + errors: [], + }); + expect(mockReadEnvFile).not.toHaveBeenCalled(); + expect(mockGetPaymentCredentialProvider).not.toHaveBeenCalled(); + expect(mockCreatePaymentCredentialProvider).not.toHaveBeenCalled(); + expect(mockUpdatePaymentCredentialProvider).not.toHaveBeenCalled(); + }); + it('creates a new credential provider when none exists', async () => { mockExistsSync.mockReturnValue(true); mockReadEnvFile.mockResolvedValue({ @@ -385,6 +423,25 @@ describe('cleanupPaymentCredentialProviders', () => { expect(mockDeletePaymentCredentialProvider).toHaveBeenCalledWith({ region: REGION, name: 'cred-three' }); }); + it('never deletes a Quick Create provider', async () => { + await cleanupPaymentCredentialProviders({ + region: REGION, + payments: { + 'my-payment-manager': { + connectors: { + quick: { + provisionMode: 'QUICK_CREATE', + credentialProviderArn: + 'arn:aws:bedrock-agentcore:us-east-1:123456789:payment-credential-provider/generated', + }, + }, + }, + }, + }); + + expect(mockDeletePaymentCredentialProvider).not.toHaveBeenCalled(); + }); + it('ignores 404 errors gracefully without throwing', async () => { mockDeletePaymentCredentialProvider.mockRejectedValue( new Error('Failed to delete payment credential provider "my-cdp-cred": Payment API error (404)') diff --git a/src/cli/operations/deploy/index.ts b/src/cli/operations/deploy/index.ts index 7e0d53e4a..cc824a529 100644 --- a/src/cli/operations/deploy/index.ts +++ b/src/cli/operations/deploy/index.ts @@ -65,6 +65,12 @@ export { export { ensureDefaultDeploymentTarget } from './ensure-target'; +export { + formatQuickCreateConnectorAuthorization, + getQuickCreateConnectorAuthorizations, + type QuickCreateConnectorAuthorization, +} from './payment-connector-authorization'; + // Pre-synth backfill of vpcId for pre-existing Container+VPC configs written before vpcId was added export { backfillContainerVpcIds, type BackfillVpcIdResult } from './backfill-vpc-id'; diff --git a/src/cli/operations/deploy/payment-connector-authorization.ts b/src/cli/operations/deploy/payment-connector-authorization.ts new file mode 100644 index 000000000..dede81371 --- /dev/null +++ b/src/cli/operations/deploy/payment-connector-authorization.ts @@ -0,0 +1,92 @@ +import type { AgentCoreProjectSpec, PaymentDeployedState } from '../../../schema'; +import { type PaymentConnectorStatus, getPaymentConnector } from '../../aws/agentcore-payments'; +import { getErrorMessage } from '../../errors'; + +export interface QuickCreateConnectorAuthorization { + managerName: string; + connectorName: string; + connectorId?: string; + status?: PaymentConnectorStatus; + authorizationUrl?: string; + error?: string; +} + +export async function getQuickCreateConnectorAuthorizations(options: { + region: string; + projectSpec: AgentCoreProjectSpec; + payments?: Record; +}): Promise { + const results: QuickCreateConnectorAuthorization[] = []; + + for (const manager of options.projectSpec.payments ?? []) { + const managerState = options.payments?.[manager.name]; + for (const connector of manager.connectors) { + if (connector.provisionMode !== 'QUICK_CREATE') continue; + + const connectorState = managerState?.connectors[connector.name]; + if (!managerState || !connectorState) { + results.push({ + managerName: manager.name, + connectorName: connector.name, + error: 'deployed connector identifiers were not found', + }); + continue; + } + + try { + const detail = await getPaymentConnector({ + region: options.region, + paymentManagerId: managerState.managerId, + paymentConnectorId: connectorState.connectorId, + }); + if (!detail) { + results.push({ + managerName: manager.name, + connectorName: connector.name, + connectorId: connectorState.connectorId, + error: 'connector was not found by the Payments service', + }); + continue; + } + + results.push({ + managerName: manager.name, + connectorName: connector.name, + connectorId: connectorState.connectorId, + status: detail.status, + authorizationUrl: detail.authorizationUrl, + }); + } catch (error) { + results.push({ + managerName: manager.name, + connectorName: connector.name, + connectorId: connectorState.connectorId, + error: getErrorMessage(error), + }); + } + } + } + + return results; +} + +export function formatQuickCreateConnectorAuthorization(result: QuickCreateConnectorAuthorization): string { + const name = `${result.managerName}/${result.connectorName}`; + + if (result.error) { + return `Payment connector "${name}" deployed, but its live status could not be retrieved: ${result.error}. Run \`agentcore status\` to retry.`; + } + if (result.status === 'PENDING_AUTHENTICATION' && result.authorizationUrl) { + return `Authorize payment connector "${name}": ${result.authorizationUrl}`; + } + if (result.status === 'PENDING_AUTHENTICATION') { + return `Payment connector "${name}" is pending authorization, but no authorization URL was returned. Run \`agentcore status\` to retry.`; + } + if (result.status === 'READY') { + return `Payment connector "${name}" is ready.`; + } + if (result.status === 'AUTHENTICATION_EXPIRED' || result.status === 'AUTHENTICATION_FAILED') { + return `Payment connector "${name}" is ${result.status}. Re-deploy it to generate a new authorization URL.`; + } + return `Payment connector "${name}" status: ${result.status ?? 'unknown'}. Run \`agentcore status\` for the latest state.`; +} diff --git a/src/cli/operations/deploy/pre-deploy-identity.ts b/src/cli/operations/deploy/pre-deploy-identity.ts index 12da31014..6bfc9e57f 100644 --- a/src/cli/operations/deploy/pre-deploy-identity.ts +++ b/src/cli/operations/deploy/pre-deploy-identity.ts @@ -281,6 +281,7 @@ export function getAllCredentials(projectSpec: AgentCoreProjectSpec): MissingCre for (const payment of projectSpec.payments ?? []) { for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; if (connector.provider === 'StripePrivy') { const vars = computeStripePrivyCredentialEnvVarNames(connector.credentialName); credentials.push( @@ -492,7 +493,9 @@ export interface SetupPaymentCredentialProvidersOptions { } export function hasPaymentCredentialProviders(projectSpec: AgentCoreProjectSpec): boolean { - return (projectSpec.payments ?? []).length > 0; + return (projectSpec.payments ?? []).some(payment => + payment.connectors.some(connector => connector.provisionMode !== 'QUICK_CREATE') + ); } export async function setupPaymentCredentialProviders( @@ -506,7 +509,7 @@ export async function setupPaymentCredentialProviders( errors: [], }; - if ((projectSpec.payments ?? []).length === 0) { + if (!hasPaymentCredentialProviders(projectSpec)) { return result; } @@ -519,6 +522,7 @@ export async function setupPaymentCredentialProviders( for (const payment of projectSpec.payments ?? []) { for (const connector of payment.connectors) { + if (connector.provisionMode === 'QUICK_CREATE') continue; try { const credentialName = connector.credentialName; const credential = projectSpec.credentials.find( @@ -572,12 +576,24 @@ export async function setupPaymentCredentialProviders( export async function cleanupPaymentCredentialProviders(options: { region: string; - payments: Record }>; + payments: Record< + string, + { + connectors?: Record< + string, + { + provisionMode?: 'MANUAL' | 'QUICK_CREATE'; + credentialProviderArn?: string; + } + >; + } + >; }): Promise { const { region, payments } = options; for (const [name, state] of Object.entries(payments)) { for (const [connName, conn] of Object.entries(state.connectors ?? {})) { + if (conn.provisionMode === 'QUICK_CREATE' || !conn.credentialProviderArn) continue; const credName = conn.credentialProviderArn.split('/').pop() ?? ''; if (credName) { try { diff --git a/src/cli/primitives/PaymentConnectorPrimitive.ts b/src/cli/primitives/PaymentConnectorPrimitive.ts index 2937c3b14..f4077d094 100644 --- a/src/cli/primitives/PaymentConnectorPrimitive.ts +++ b/src/cli/primitives/PaymentConnectorPrimitive.ts @@ -1,6 +1,11 @@ import { findConfigRoot, removeEnvVars, setEnvVar, toError } from '../../lib'; import type { AgentCoreProjectSpec, PaymentProvider } from '../../schema'; -import { PaymentConnectorNameSchema, PaymentConnectorSchema, PaymentProviderSchema } from '../../schema'; +import { + PaymentConnectorNameSchema, + PaymentConnectorSchema, + PaymentProviderSchema, + PaymentProvisionModeSchema, +} from '../../schema'; import type { RemoveResult } from '../commands/remove/types'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; @@ -25,6 +30,7 @@ export interface AddCoinbaseCdpConnectorOptions { manager: string; name: string; provider: 'CoinbaseCDP'; + provisionMode?: 'MANUAL'; apiKeyId: string; apiKeySecret: string; walletSecret: string; @@ -37,13 +43,24 @@ export interface AddStripePrivyConnectorOptions { manager: string; name: string; provider: 'StripePrivy'; + provisionMode?: 'MANUAL'; appId: string; appSecret: string; authorizationPrivateKey: string; authorizationId: string; } -export type AddPaymentConnectorOptions = AddCoinbaseCdpConnectorOptions | AddStripePrivyConnectorOptions; +export interface AddQuickCreateConnectorOptions { + manager: string; + name: string; + provider: 'CoinbaseCDP'; + provisionMode: 'QUICK_CREATE'; +} + +export type AddPaymentConnectorOptions = + | AddCoinbaseCdpConnectorOptions + | AddStripePrivyConnectorOptions + | AddQuickCreateConnectorOptions; /** * Removable connector resource with parent manager context. @@ -64,7 +81,7 @@ export class PaymentConnectorPrimitive extends BasePrimitive> { + ): Promise> { try { const project = await this.readProjectSpec(); // payments is optional in the schema; a connector can only attach to an @@ -84,6 +101,22 @@ export class PaymentConnectorPrimitive extends BasePrimitive c.name === resolvedConnector); if (connIndex !== -1) { const connector = manager.connectors[connIndex]!; - const credentialName = connector.credentialName; // Remove connector manager.connectors.splice(connIndex, 1); - // Remove associated credential if no longer referenced - const stillReferenced = project.payments.some(m => - m.connectors.some(c => c.credentialName === credentialName) - ); - if (!stillReferenced) { - const credIndex = project.credentials.findIndex(c => c.name === credentialName); - if (credIndex !== -1) { - project.credentials.splice(credIndex, 1); + if (connector.provisionMode !== 'QUICK_CREATE') { + const credentialName = connector.credentialName; + const stillReferenced = project.payments.some(m => + m.connectors.some(c => c.provisionMode !== 'QUICK_CREATE' && c.credentialName === credentialName) + ); + if (!stillReferenced) { + const credIndex = project.credentials.findIndex(c => c.name === credentialName); + if (credIndex !== -1) { + project.credentials.splice(credIndex, 1); + } } - } - await this.writeProjectSpec(project); - - // Clean up .env.local secrets (provider-specific) - if (!stillReferenced) { - try { - if (connector.provider === 'StripePrivy') { - const envVarNames = computeStripePrivyCredentialEnvVarNames(credentialName); - await removeEnvVars([ - envVarNames.appId, - envVarNames.appSecret, - envVarNames.authorizationPrivateKey, - envVarNames.authorizationId, - ]); - } else { - const envVarNames = computePaymentCredentialEnvVarNames(credentialName); - await removeEnvVars([envVarNames.apiKeyId, envVarNames.apiKeySecret, envVarNames.walletSecret]); + if (!stillReferenced) { + try { + if (connector.provider === 'StripePrivy') { + const envVarNames = computeStripePrivyCredentialEnvVarNames(credentialName); + await removeEnvVars([ + envVarNames.appId, + envVarNames.appSecret, + envVarNames.authorizationPrivateKey, + envVarNames.authorizationId, + ]); + } else { + const envVarNames = computePaymentCredentialEnvVarNames(credentialName); + await removeEnvVars([envVarNames.apiKeyId, envVarNames.apiKeySecret, envVarNames.walletSecret]); + } + } catch { + // Best-effort cleanup } - } catch { - // Best-effort cleanup } } + await this.writeProjectSpec(project); + return { success: true }; } } @@ -257,15 +290,17 @@ export class PaymentConnectorPrimitive extends BasePrimitive - m.connectors - .filter(c => !(m.name === manager.name && c.name === targetConnector)) - .some(c => c.credentialName === connector.credentialName) - ); - if (!stillReferenced) { - summary.push(`Associated credential "${connector.credentialName}" will also be removed`); - } else { - summary.push(`Credential "${connector.credentialName}" is shared and will be kept`); + if (connector.provisionMode !== 'QUICK_CREATE') { + const stillReferenced = project.payments.some(m => + m.connectors + .filter(c => !(m.name === manager.name && c.name === targetConnector)) + .some(c => c.provisionMode !== 'QUICK_CREATE' && c.credentialName === connector.credentialName) + ); + if (!stillReferenced) { + summary.push(`Associated credential "${connector.credentialName}" will also be removed`); + } else { + summary.push(`Credential "${connector.credentialName}" is shared and will be kept`); + } } const schemaChanges: SchemaChange[] = []; @@ -321,10 +356,11 @@ export class PaymentConnectorPrimitive extends BasePrimitive', 'Payment manager name [non-interactive]') .option('--name ', 'Payment connector name [non-interactive]') .option('--provider ', 'Payment provider: CoinbaseCDP, StripePrivy [non-interactive]') + .option('--provision-mode ', 'Provisioning mode: MANUAL, QUICK_CREATE [non-interactive]') .option('--api-key-id ', 'CDP API Key ID (CoinbaseCDP) [non-interactive]') .option('--api-key-secret ', 'CDP API Key Secret (CoinbaseCDP) [non-interactive]') .option('--wallet-secret ', 'CDP Wallet Secret (CoinbaseCDP) [non-interactive]') @@ -338,6 +374,7 @@ export class PaymentConnectorPrimitive extends BasePrimitive { if (cliOptions.json) { console.log(JSON.stringify({ success: false, error })); } else { console.error(error); } process.exit(1); + }; + + const provisionModeResult = PaymentProvisionModeSchema.safeParse(cliOptions.provisionMode ?? 'MANUAL'); + if (!provisionModeResult.success) { + failValidation(`Invalid provision mode "${cliOptions.provisionMode}". Valid: MANUAL, QUICK_CREATE`); } - let provider: PaymentProvider; - try { - provider = PaymentProviderSchema.parse(cliOptions.provider); - } catch { - const error = `Invalid provider "${cliOptions.provider}". Valid: CoinbaseCDP, StripePrivy`; - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); + const provisionMode = provisionModeResult.data; + + let provider: PaymentProvider = 'CoinbaseCDP'; + if (provisionMode === 'QUICK_CREATE') { + if (cliOptions.provider && cliOptions.provider !== 'CoinbaseCDP') { + failValidation('QUICK_CREATE only supports the CoinbaseCDP provider'); + } + provider = 'CoinbaseCDP'; + } else { + if (!cliOptions.provider) { + failValidation('--provider is required. Valid: CoinbaseCDP, StripePrivy'); + } + try { + provider = PaymentProviderSchema.parse(cliOptions.provider); + } catch { + failValidation(`Invalid provider "${cliOptions.provider}". Valid: CoinbaseCDP, StripePrivy`); } - process.exit(1); } const missing: string[] = []; if (!cliOptions.manager) missing.push('--manager'); if (!cliOptions.name) missing.push('--name'); - if (provider === 'StripePrivy') { + if (provisionMode === 'QUICK_CREATE') { + const suppliedCredentialOptions = [ + cliOptions.apiKeyId !== undefined && '--api-key-id', + cliOptions.apiKeySecret !== undefined && '--api-key-secret', + cliOptions.walletSecret !== undefined && '--wallet-secret', + cliOptions.appId !== undefined && '--app-id', + cliOptions.appSecret !== undefined && '--app-secret', + cliOptions.authorizationPrivateKey !== undefined && '--authorization-private-key', + cliOptions.authorizationId !== undefined && '--authorization-id', + ].filter((option): option is string => Boolean(option)); + if (suppliedCredentialOptions.length > 0) { + failValidation( + `Credential options cannot be used with QUICK_CREATE: ${suppliedCredentialOptions.join(', ')}` + ); + } + } else if (provider === 'StripePrivy') { if (!cliOptions.appId?.trim()) missing.push('--app-id'); if (!cliOptions.appSecret?.trim()) missing.push('--app-secret'); if (!cliOptions.authorizationPrivateKey?.trim()) missing.push('--authorization-private-key'); @@ -425,47 +488,45 @@ export class PaymentConnectorPrimitive extends BasePrimitive { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); + if (provisionMode === 'MANUAL') { + // Emit the leak warning BEFORE format validation: a literal secret + // is already exposed to shell history / the process table the moment + // the command is typed, regardless of whether the value is well-formed. + // Warning after validation would silently skip the malformed-secret + // case — exactly where the leak still happened. + warnOnLiteralSecretFlag( + [ + cliOptions.apiKeySecret, + cliOptions.walletSecret, + cliOptions.appSecret, + cliOptions.authorizationPrivateKey, + ], + cliOptions.json, + 'add payment-connector' + ); + + if (provider === 'StripePrivy') { + // AWS docs ship the key with a `wallet-auth:` prefix — strip it transparently. + cliOptions.authorizationPrivateKey = stripWalletAuthPrefix(cliOptions.authorizationPrivateKey!); + const keyResult = validateAuthorizationPrivateKey(cliOptions.authorizationPrivateKey); + if (keyResult !== true) failValidation(keyResult); } else { - console.error(error); + const apiKeySecretResult = validateApiKeySecret(cliOptions.apiKeySecret!); + if (apiKeySecretResult !== true) failValidation(apiKeySecretResult); + const walletSecretResult = validateWalletSecret(cliOptions.walletSecret!); + if (walletSecretResult !== true) failValidation(walletSecretResult); } - process.exit(1); - }; - - // Emit the leak warning BEFORE format validation: a literal secret - // is already exposed to shell history / the process table the moment - // the command is typed, regardless of whether the value is well-formed. - // Warning after validation would silently skip the malformed-secret - // case — exactly where the leak still happened. - warnOnLiteralSecretFlag( - [ - cliOptions.apiKeySecret, - cliOptions.walletSecret, - cliOptions.appSecret, - cliOptions.authorizationPrivateKey, - ], - cliOptions.json, - 'add payment-connector' - ); - - if (provider === 'StripePrivy') { - // AWS docs ship the key with a `wallet-auth:` prefix — strip it transparently. - cliOptions.authorizationPrivateKey = stripWalletAuthPrefix(cliOptions.authorizationPrivateKey!); - const keyResult = validateAuthorizationPrivateKey(cliOptions.authorizationPrivateKey); - if (keyResult !== true) failValidation(keyResult); - } else { - const apiKeySecretResult = validateApiKeySecret(cliOptions.apiKeySecret!); - if (apiKeySecretResult !== true) failValidation(apiKeySecretResult); - const walletSecretResult = validateWalletSecret(cliOptions.walletSecret!); - if (walletSecretResult !== true) failValidation(walletSecretResult); } let result: Awaited>; - if (provider === 'StripePrivy') { + if (provisionMode === 'QUICK_CREATE') { + result = await this.add({ + manager: cliOptions.manager!, + name: cliOptions.name!, + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }); + } else if (provider === 'StripePrivy') { result = await this.add({ manager: cliOptions.manager!, name: cliOptions.name!, @@ -499,8 +560,12 @@ export class PaymentConnectorPrimitive extends BasePrimitive', 'Name of connector to remove [non-interactive]') .option('--manager ', 'Payment manager name [non-interactive]') .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') diff --git a/src/cli/primitives/PaymentManagerPrimitive.ts b/src/cli/primitives/PaymentManagerPrimitive.ts index 5b475c0b9..7b0a2ff14 100644 --- a/src/cli/primitives/PaymentManagerPrimitive.ts +++ b/src/cli/primitives/PaymentManagerPrimitive.ts @@ -222,10 +222,12 @@ export class PaymentManagerPrimitive extends BasePrimitive ({ - credentialName: c.credentialName, - provider: c.provider, - })); + const connectorInfo = manager.connectors + .filter(c => c.provisionMode !== 'QUICK_CREATE') + .map(c => ({ + credentialName: c.credentialName, + provider: c.provider, + })); // Remove the manager (which removes all its nested connectors) project.payments.splice(index, 1); @@ -285,14 +287,24 @@ export class PaymentManagerPrimitive extends BasePrimitive 0) { summary.push(`Note: ${manager.connectors.length} connector(s) within this manager will also be removed`); for (const conn of manager.connectors) { - summary.push(` - Connector: ${conn.name} (credential: ${conn.credentialName})`); + summary.push( + conn.provisionMode === 'QUICK_CREATE' + ? ` - Connector: ${conn.name} (Quick Create)` + : ` - Connector: ${conn.name} (credential: ${conn.credentialName})` + ); } } - const credentialNames = manager.connectors.map(c => c.credentialName); + const credentialNames = manager.connectors + .filter(connector => connector.provisionMode !== 'QUICK_CREATE') + .map(connector => connector.credentialName); for (const credName of credentialNames) { const otherReferences = project.payments.some( - m => m.name !== name && m.connectors.some(c => c.credentialName === credName) + manager => + manager.name !== name && + manager.connectors.some( + connector => connector.provisionMode !== 'QUICK_CREATE' && connector.credentialName === credName + ) ); if (!otherReferences) { summary.push(`Associated credential "${credName}" will also be removed`); @@ -336,7 +348,7 @@ export class PaymentManagerPrimitive extends BasePrimitive', 'Payment manager name [non-interactive]') .option('--authorizer-type ', 'Authorizer type: AWS_IAM or CUSTOM_JWT (default: AWS_IAM) [non-interactive]') .option('--discovery-url ', 'OIDC discovery URL (required for CUSTOM_JWT) [non-interactive]') @@ -519,7 +531,7 @@ export class PaymentManagerPrimitive extends BasePrimitive', 'Name of resource to remove [non-interactive]') .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') .option('--json', 'Output as JSON [non-interactive]') diff --git a/src/cli/primitives/__tests__/PaymentConnectorPrimitive.test.ts b/src/cli/primitives/__tests__/PaymentConnectorPrimitive.test.ts index 48189aa6f..647b90454 100644 --- a/src/cli/primitives/__tests__/PaymentConnectorPrimitive.test.ts +++ b/src/cli/primitives/__tests__/PaymentConnectorPrimitive.test.ts @@ -59,7 +59,7 @@ function makeProject(overrides: Partial = {}): AgentCorePr function makeManager( name: string, - connectors: { name: string; provider: 'CoinbaseCDP' | 'StripePrivy'; credentialName: string }[] = [] + connectors: NonNullable[number]['connectors'] = [] ) { return { name, @@ -224,6 +224,55 @@ describe('PaymentConnectorPrimitive', () => { }); }); + describe('Quick Create happy path', () => { + it('writes a Quick Create connector without credentials or env vars', async () => { + mockReadProjectSpec.mockResolvedValue(makeProject({ payments: [makeManager('mgr1')] })); + + const result = await primitive.add({ + manager: 'mgr1', + name: 'quickConn', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }); + + expect(result.success).toBe(true); + if (!result.success) throw new Error('expected success'); + expect(result.credentialName).toBeUndefined(); + expect(mockSetEnvVar).not.toHaveBeenCalled(); + expect(mockWriteProjectSpec).toHaveBeenCalledTimes(1); + + const writtenSpec = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; + expect(writtenSpec.credentials).toEqual([]); + expect(writtenSpec.payments?.[0]?.connectors).toEqual([ + { + name: 'quickConn', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ]); + }); + + it('still rejects duplicate connector names', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject({ + payments: [ + makeManager('mgr1', [{ name: 'quickConn', provider: 'CoinbaseCDP', provisionMode: 'QUICK_CREATE' }]), + ], + }) + ); + + const result = await primitive.add({ + manager: 'mgr1', + name: 'quickConn', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }); + + expect(result.success).toBe(false); + expect(mockWriteProjectSpec).not.toHaveBeenCalled(); + }); + }); + describe('error cases', () => { it('returns error when manager does not exist', async () => { mockReadProjectSpec.mockResolvedValue(makeProject({ payments: [] })); @@ -378,6 +427,24 @@ describe('PaymentConnectorPrimitive', () => { // No env var cleanup expect(mockRemoveEnvVars).not.toHaveBeenCalled(); }); + + it('removes a Quick Create connector without touching credentials or env vars', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject({ + payments: [ + makeManager('mgr1', [{ name: 'quickConn', provider: 'CoinbaseCDP', provisionMode: 'QUICK_CREATE' }]), + ], + }) + ); + + const result = await primitive.remove('mgr1/quickConn'); + + expect(result.success).toBe(true); + const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; + expect(written.payments?.[0]?.connectors).toEqual([]); + expect(written.credentials).toEqual([]); + expect(mockRemoveEnvVars).not.toHaveBeenCalled(); + }); }); // ── previewRemove() ──────────────────────────────────────────────────────── @@ -423,6 +490,20 @@ describe('PaymentConnectorPrimitive', () => { expect(sharedMsg).toBeDefined(); }); + it('does not describe credential cleanup for a Quick Create connector', async () => { + mockReadProjectSpec.mockResolvedValue( + makeProject({ + payments: [ + makeManager('mgr1', [{ name: 'quickConn', provider: 'CoinbaseCDP', provisionMode: 'QUICK_CREATE' }]), + ], + }) + ); + + const preview = await primitive.previewRemove('mgr1/quickConn'); + + expect(preview.summary).toEqual(['Removing payment connector: quickConn (from manager mgr1)']); + }); + it('includes the target connector in the summary', async () => { mockReadProjectSpec.mockResolvedValue( makeProject({ diff --git a/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts b/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts index c98497e82..d1d9cf219 100644 --- a/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts +++ b/src/cli/primitives/__tests__/PaymentManagerPrimitive.test.ts @@ -2,8 +2,11 @@ import type { AgentCoreProjectSpec } from '../../../schema'; import { PaymentManagerPrimitive } from '../PaymentManagerPrimitive'; import { afterEach, describe, expect, it, vi } from 'vitest'; -const mockReadProjectSpec = vi.fn(); -const mockWriteProjectSpec = vi.fn(); +const { mockReadProjectSpec, mockWriteProjectSpec, mockRemoveEnvVars } = vi.hoisted(() => ({ + mockReadProjectSpec: vi.fn(), + mockWriteProjectSpec: vi.fn(), + mockRemoveEnvVars: vi.fn().mockResolvedValue(undefined), +})); vi.mock('../../../lib', () => ({ ConfigIO: class { @@ -11,7 +14,7 @@ vi.mock('../../../lib', () => ({ writeProjectSpec = mockWriteProjectSpec; }, findConfigRoot: vi.fn().mockReturnValue(null), - removeEnvVars: vi.fn().mockResolvedValue(undefined), + removeEnvVars: mockRemoveEnvVars, toError: (err: unknown) => (err instanceof Error ? err : new Error(String(err))), serializeResult: (r: unknown) => r, })); @@ -274,6 +277,39 @@ describe('PaymentManagerPrimitive', () => { expect(written.credentials).toHaveLength(0); }); + it('removes a mixed manual and Quick Create manager without cleaning generated credentials', async () => { + const project = makeProject({ + payments: [ + { + ...makePaymentManager('mixedManager'), + connectors: [ + { + name: 'manual', + provider: 'CoinbaseCDP', + credentialName: 'manualCred', + }, + { + name: 'quick', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ], + }, + ], + credentials: [makePaymentCredential('manualCred')], + }); + mockReadProjectSpec.mockResolvedValue(project); + mockWriteProjectSpec.mockResolvedValue(undefined); + + const result = await primitive.remove('mixedManager'); + + expect(result.success).toBe(true); + const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; + expect(written.payments).toHaveLength(0); + expect(written.credentials).toHaveLength(0); + expect(mockRemoveEnvVars).toHaveBeenCalledTimes(1); + }); + it('non-existent name — returns error without writing', async () => { mockReadProjectSpec.mockResolvedValue(makeProject()); @@ -417,6 +453,38 @@ describe('PaymentManagerPrimitive', () => { expect(preview.summary.some(s => s.includes('kept'))).toBe(true); }); + it('describes Quick Create connectors without undefined credential cleanup', async () => { + const project = makeProject({ + payments: [ + { + ...makePaymentManager('mixedManager'), + connectors: [ + { + name: 'manual', + provider: 'CoinbaseCDP', + credentialName: 'manualCred', + }, + { + name: 'quick', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }, + ], + }, + ], + credentials: [makePaymentCredential('manualCred')], + }); + mockReadProjectSpec.mockResolvedValue(project); + + const preview = await primitive.previewRemove('mixedManager'); + const summary = preview.summary.join('\n'); + + expect(summary).toContain('Connector: manual (credential: manualCred)'); + expect(summary).toContain('Connector: quick (Quick Create)'); + expect(summary).toContain('Associated credential "manualCred" will also be removed'); + expect(summary).not.toContain('undefined'); + }); + it('throws when manager not found', async () => { mockReadProjectSpec.mockResolvedValue(makeProject()); diff --git a/src/cli/tui/screens/add/AddScreen.tsx b/src/cli/tui/screens/add/AddScreen.tsx index 5922b47f3..e147324fe 100644 --- a/src/cli/tui/screens/add/AddScreen.tsx +++ b/src/cli/tui/screens/add/AddScreen.tsx @@ -33,10 +33,10 @@ const BASE_ADD_RESOURCES: { id: AddResourceType; title: string; description: str { id: 'policy', title: 'Policy', description: 'Cedar policies for gateway tools' }, { id: 'dataset', title: 'Dataset', description: 'Evaluation dataset for testing agents' }, { id: 'config-bundle', title: 'Configuration Bundle', description: 'Versioned component configurations' }, - { id: 'payment-manager', title: 'Payment Manager [preview]', description: 'x402 crypto microtransactions config' }, + { id: 'payment-manager', title: 'Payment Manager', description: 'x402 crypto microtransactions config' }, { id: 'payment-connector', - title: 'Payment Connector [preview]', + title: 'Payment Connector', description: 'Link payment provider credentials to a manager', }, ]; diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index 0d7af8d1e..815c7ac3f 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -5,6 +5,7 @@ import type { CdkToolkitWrapper, DeployMessage, SwitchableIoHost } from '../../. import { buildDeployedState, getStackOutputs, + omitPaymentAuthorizationOutputs, parseAgentOutputs, parseConfigBundleOutputs, parseDatasetOutputs, @@ -26,6 +27,8 @@ import { ExecLogger } from '../../../logging'; import { MANAGED_MEMORY_DEPLOY_NOTICE, cleanupPaymentCredentialProviders, + formatQuickCreateConnectorAuthorization, + getQuickCreateConnectorAuthorizations, hasManagedMemoryHarness, performStackTeardown, setupTransactionSearch, @@ -491,30 +494,27 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState } // Expose outputs to UI - setStackOutputs(outputs); + setStackOutputs(omitPaymentAuthorizationOutputs(outputs)); // Parse payment outputs from CFN stack - const paymentSpecs = (ctx.projectSpec.payments ?? []).map( - (p: { - name: string; - authorizerType?: 'AWS_IAM' | 'CUSTOM_JWT'; - autoPayment?: boolean; - paymentToolAllowlist?: string[]; - networkPreferences?: string[]; - connectors: { name: string; credentialName: string }[]; - }) => ({ - name: p.name, - authorizerType: p.authorizerType, - autoPayment: p.autoPayment, - paymentToolAllowlist: p.paymentToolAllowlist, - networkPreferences: p.networkPreferences, - connectors: p.connectors.map(c => ({ + const paymentSpecs = (ctx.projectSpec.payments ?? []).map(p => ({ + name: p.name, + authorizerType: p.authorizerType, + autoPayment: p.autoPayment, + paymentToolAllowlist: p.paymentToolAllowlist, + networkPreferences: p.networkPreferences, + connectors: p.connectors.map(c => { + if (c.provisionMode === 'QUICK_CREATE') { + return { name: c.name, provisionMode: 'QUICK_CREATE' as const }; + } + return { name: c.name, - credentialProviderArn: allCredentials[c.credentialName]?.credentialProviderArn ?? '', + ...(c.provisionMode && { provisionMode: c.provisionMode }), + credentialProviderArn: allCredentials[c.credentialName]?.credentialProviderArn, credentialProviderName: c.credentialName, - })), - }) - ); + }; + }), + })); const payments = paymentSpecs.length > 0 ? parsePaymentOutputs(outputs, paymentSpecs) : undefined; const existingState = await configIO.readDeployedState().catch(() => undefined); @@ -560,6 +560,13 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState logger.endStep('success'); setPersistStateStep(prev => ({ ...prev, status: 'success' })); + const quickCreateAuthorizations = await getQuickCreateConnectorAuthorizations({ + region: target.region, + projectSpec: ctx.projectSpec, + payments, + }); + setDeployNotes(prev => [...prev, ...quickCreateAuthorizations.map(formatQuickCreateConnectorAuthorization)]); + // Post-deploy: auto-trigger ingestion for any KB whose data-source URIs // changed since the last deploy (or has never been ingested before). const knowledgeBaseSpecsForIngest = ctx.projectSpec.knowledgeBases ?? []; diff --git a/src/cli/tui/screens/payment/AddPaymentConnectorScreen.tsx b/src/cli/tui/screens/payment/AddPaymentConnectorScreen.tsx index 12a81f5ab..7d2b080c9 100644 --- a/src/cli/tui/screens/payment/AddPaymentConnectorScreen.tsx +++ b/src/cli/tui/screens/payment/AddPaymentConnectorScreen.tsx @@ -1,4 +1,3 @@ -import type { PaymentProvider } from '../../../../schema'; import { PaymentConnectorNameSchema } from '../../../../schema'; import { validateApiKeySecret, @@ -10,8 +9,8 @@ import type { SelectableItem } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation } from '../../hooks'; import { generateUniqueName } from '../../utils'; -import type { AddPaymentConnectorConfig } from './types'; -import { CONNECTOR_STEP_LABELS, PAYMENT_PROVIDER_OPTIONS } from './types'; +import type { AddPaymentConnectorConfig, PaymentConnectorSetup } from './types'; +import { CONNECTOR_STEP_LABELS, PAYMENT_CONNECTOR_SETUP_OPTIONS } from './types'; import { useAddPaymentConnectorWizard } from './useAddPaymentWizard'; import React, { useMemo } from 'react'; @@ -50,13 +49,13 @@ export function AddPaymentConnectorScreen({ [existingManagerNames] ); - const providerItems: SelectableItem[] = useMemo( - () => PAYMENT_PROVIDER_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })), + const setupItems: SelectableItem[] = useMemo( + () => PAYMENT_CONNECTOR_SETUP_OPTIONS.map(opt => ({ id: opt.id, title: opt.title, description: opt.description })), [] ); const isManagerSelectStep = wizard.step === 'manager-select'; - const isProviderStep = wizard.step === 'provider-select'; + const isSetupStep = wizard.step === 'setup-select'; const isApiKeyIdStep = wizard.step === 'api-key-id'; const isApiKeySecretStep = wizard.step === 'api-key-secret'; const isWalletSecretStep = wizard.step === 'wallet-secret'; @@ -77,9 +76,9 @@ export function AddPaymentConnectorScreen({ isActive: isManagerSelectStep, }); - const providerNav = useListNavigation({ - items: providerItems, - onSelect: item => wizard.setProvider(item.id as PaymentProvider), + const setupNav = useListNavigation({ + items: setupItems, + onSelect: item => wizard.setSetup(item.id as PaymentConnectorSetup), onExit: () => { if (wizard.currentIndex === 0) { onExit(); @@ -87,7 +86,7 @@ export function AddPaymentConnectorScreen({ wizard.goBack(); } }, - isActive: isProviderStep, + isActive: isSetupStep, }); useListNavigation({ @@ -98,7 +97,7 @@ export function AddPaymentConnectorScreen({ }); const helpText = - isManagerSelectStep || isProviderStep + isManagerSelectStep || isSetupStep ? HELP_TEXT.NAVIGATE_SELECT : isConfirmStep ? HELP_TEXT.CONFIRM_CANCEL @@ -128,12 +127,12 @@ export function AddPaymentConnectorScreen({ /> )} - {isProviderStep && ( + {isSetupStep && ( )} @@ -238,69 +237,78 @@ export function AddPaymentConnectorScreen({ 8 - ? '****' + wizard.config.appId.slice(-4) - : '••••••••' - : '', - }, - { - label: 'App Secret', - value: wizard.config.appSecret - ? wizard.config.appSecret.length > 8 - ? '****' + wizard.config.appSecret.slice(-4) - : '••••••••' - : '', - }, - { - label: 'Authorization Private Key', - value: wizard.config.authorizationPrivateKey - ? wizard.config.authorizationPrivateKey.length > 8 - ? '****' + wizard.config.authorizationPrivateKey.slice(-4) - : '••••••••' - : '', - }, - { - label: 'Authorization ID', - value: wizard.config.authorizationId - ? wizard.config.authorizationId.length > 8 - ? '****' + wizard.config.authorizationId.slice(-4) - : '••••••••' - : '', - }, - ] - : [ - { - label: 'API Key ID', - value: wizard.config.apiKeyId - ? wizard.config.apiKeyId.length > 8 - ? '****' + wizard.config.apiKeyId.slice(-4) - : '••••••••' - : '', - }, - { - label: 'API Key Secret', - value: wizard.config.apiKeySecret - ? wizard.config.apiKeySecret.length > 8 - ? '****' + wizard.config.apiKeySecret.slice(-4) - : '••••••••' - : '', - }, - { - label: 'Wallet Secret', - value: wizard.config.walletSecret - ? wizard.config.walletSecret.length > 8 - ? '****' + wizard.config.walletSecret.slice(-4) - : '••••••••' - : '', - }, - ]), + ...(wizard.config.provisionMode === 'QUICK_CREATE' + ? [] + : wizard.config.provider === 'StripePrivy' + ? [ + { + label: 'App ID', + value: wizard.config.appId + ? wizard.config.appId.length > 8 + ? '****' + wizard.config.appId.slice(-4) + : '••••••••' + : '', + }, + { + label: 'App Secret', + value: wizard.config.appSecret + ? wizard.config.appSecret.length > 8 + ? '****' + wizard.config.appSecret.slice(-4) + : '••••••••' + : '', + }, + { + label: 'Authorization Private Key', + value: wizard.config.authorizationPrivateKey + ? wizard.config.authorizationPrivateKey.length > 8 + ? '****' + wizard.config.authorizationPrivateKey.slice(-4) + : '••••••••' + : '', + }, + { + label: 'Authorization ID', + value: wizard.config.authorizationId + ? wizard.config.authorizationId.length > 8 + ? '****' + wizard.config.authorizationId.slice(-4) + : '••••••••' + : '', + }, + ] + : [ + { + label: 'API Key ID', + value: wizard.config.apiKeyId + ? wizard.config.apiKeyId.length > 8 + ? '****' + wizard.config.apiKeyId.slice(-4) + : '••••••••' + : '', + }, + { + label: 'API Key Secret', + value: wizard.config.apiKeySecret + ? wizard.config.apiKeySecret.length > 8 + ? '****' + wizard.config.apiKeySecret.slice(-4) + : '••••••••' + : '', + }, + { + label: 'Wallet Secret', + value: wizard.config.walletSecret + ? wizard.config.walletSecret.length > 8 + ? '****' + wizard.config.walletSecret.slice(-4) + : '••••••••' + : '', + }, + ]), ]} /> )} diff --git a/src/cli/tui/screens/payment/AddPaymentFlow.tsx b/src/cli/tui/screens/payment/AddPaymentFlow.tsx index fca312e41..f51789c8a 100644 --- a/src/cli/tui/screens/payment/AddPaymentFlow.tsx +++ b/src/cli/tui/screens/payment/AddPaymentFlow.tsx @@ -18,7 +18,7 @@ type FlowState = | { name: 'connector-wizard-unified'; managerConfig: AddPaymentManagerConfig } | { name: 'confirm'; managerConfig: AddPaymentManagerConfig; connectorConfig?: AddPaymentConnectorConfig } | { name: 'connector-wizard'; preSelectedManager?: string } - | { name: 'success'; message: string } + | { name: 'success'; message: string; detail?: string } | { name: 'error'; message: string }; interface AddPaymentFlowProps { @@ -159,28 +159,41 @@ export function AddPaymentFlow({ } as const; const connectorOptions = - config.provider === 'StripePrivy' + config.provisionMode === 'QUICK_CREATE' ? { - ...baseOptions, - provider: 'StripePrivy' as const, - appId: config.appId, - appSecret: config.appSecret, - authorizationPrivateKey: config.authorizationPrivateKey, - authorizationId: config.authorizationId, - } - : { ...baseOptions, provider: 'CoinbaseCDP' as const, - apiKeyId: config.apiKeyId, - apiKeySecret: config.apiKeySecret, - walletSecret: config.walletSecret, - }; + provisionMode: 'QUICK_CREATE' as const, + } + : config.provider === 'StripePrivy' + ? { + ...baseOptions, + provider: 'StripePrivy' as const, + appId: config.appId, + appSecret: config.appSecret, + authorizationPrivateKey: config.authorizationPrivateKey, + authorizationId: config.authorizationId, + } + : { + ...baseOptions, + provider: 'CoinbaseCDP' as const, + apiKeyId: config.apiKeyId, + apiKeySecret: config.apiKeySecret, + walletSecret: config.walletSecret, + }; setFlow({ name: 'loading' }); void createConnector(connectorOptions) .then(result => { if (result.ok) { - setFlow({ name: 'success', message: `Added payment connector: ${result.connectorName}` }); + setFlow({ + name: 'success', + message: `Added payment connector: ${result.connectorName}`, + detail: + config.provisionMode === 'QUICK_CREATE' + ? 'Run `agentcore deploy` to create the connector and receive its authorization URL.' + : undefined, + }); } else { setFlow({ name: 'error', message: result.error }); } @@ -293,59 +306,70 @@ export function AddPaymentFlow({ const connectorFields = flow.connectorConfig ? [ { label: 'Connector Name', value: flow.connectorConfig.connectorName }, - { label: 'Provider', value: flow.connectorConfig.provider }, - ...(flow.connectorConfig.provider === 'StripePrivy' - ? [ - { - label: 'App ID', - value: - flow.connectorConfig.appId.length > 8 ? '****' + flow.connectorConfig.appId.slice(-4) : '••••••••', - }, - { - label: 'App Secret', - value: - flow.connectorConfig.appSecret.length > 8 - ? '****' + flow.connectorConfig.appSecret.slice(-4) - : '••••••••', - }, - { - label: 'Auth Key', - value: - flow.connectorConfig.authorizationPrivateKey.length > 8 - ? '****' + flow.connectorConfig.authorizationPrivateKey.slice(-4) - : '••••••••', - }, - { - label: 'Auth ID', - value: - flow.connectorConfig.authorizationId.length > 8 - ? '****' + flow.connectorConfig.authorizationId.slice(-4) - : '••••••••', - }, - ] - : [ - { - label: 'API Key ID', - value: - flow.connectorConfig.apiKeyId.length > 8 - ? '****' + flow.connectorConfig.apiKeyId.slice(-4) - : '••••••••', - }, - { - label: 'API Key Secret', - value: - flow.connectorConfig.apiKeySecret.length > 8 - ? '****' + flow.connectorConfig.apiKeySecret.slice(-4) - : '••••••••', - }, - { - label: 'Wallet Secret', - value: - flow.connectorConfig.walletSecret.length > 8 - ? '****' + flow.connectorConfig.walletSecret.slice(-4) - : '••••••••', - }, - ]), + { + label: 'Provisioning', + value: flow.connectorConfig.provisionMode === 'QUICK_CREATE' ? 'Quick Create' : 'Manual', + }, + { + label: 'Provider', + value: flow.connectorConfig.provider === 'StripePrivy' ? 'Stripe + Privy' : 'Coinbase CDP', + }, + ...(flow.connectorConfig.provisionMode === 'QUICK_CREATE' + ? [] + : flow.connectorConfig.provider === 'StripePrivy' + ? [ + { + label: 'App ID', + value: + flow.connectorConfig.appId.length > 8 + ? '****' + flow.connectorConfig.appId.slice(-4) + : '••••••••', + }, + { + label: 'App Secret', + value: + flow.connectorConfig.appSecret.length > 8 + ? '****' + flow.connectorConfig.appSecret.slice(-4) + : '••••••••', + }, + { + label: 'Auth Key', + value: + flow.connectorConfig.authorizationPrivateKey.length > 8 + ? '****' + flow.connectorConfig.authorizationPrivateKey.slice(-4) + : '••••••••', + }, + { + label: 'Auth ID', + value: + flow.connectorConfig.authorizationId.length > 8 + ? '****' + flow.connectorConfig.authorizationId.slice(-4) + : '••••••••', + }, + ] + : [ + { + label: 'API Key ID', + value: + flow.connectorConfig.apiKeyId.length > 8 + ? '****' + flow.connectorConfig.apiKeyId.slice(-4) + : '••••••••', + }, + { + label: 'API Key Secret', + value: + flow.connectorConfig.apiKeySecret.length > 8 + ? '****' + flow.connectorConfig.apiKeySecret.slice(-4) + : '••••••••', + }, + { + label: 'Wallet Secret', + value: + flow.connectorConfig.walletSecret.length > 8 + ? '****' + flow.connectorConfig.walletSecret.slice(-4) + : '••••••••', + }, + ]), ] : []; @@ -406,22 +430,28 @@ export function AddPaymentFlow({ provider: connConfig.provider, } as const; const connectorOptions = - connConfig.provider === 'StripePrivy' + connConfig.provisionMode === 'QUICK_CREATE' ? { - ...baseOptions, - provider: 'StripePrivy' as const, - appId: connConfig.appId, - appSecret: connConfig.appSecret, - authorizationPrivateKey: connConfig.authorizationPrivateKey, - authorizationId: connConfig.authorizationId, - } - : { ...baseOptions, provider: 'CoinbaseCDP' as const, - apiKeyId: connConfig.apiKeyId, - apiKeySecret: connConfig.apiKeySecret, - walletSecret: connConfig.walletSecret, - }; + provisionMode: 'QUICK_CREATE' as const, + } + : connConfig.provider === 'StripePrivy' + ? { + ...baseOptions, + provider: 'StripePrivy' as const, + appId: connConfig.appId, + appSecret: connConfig.appSecret, + authorizationPrivateKey: connConfig.authorizationPrivateKey, + authorizationId: connConfig.authorizationId, + } + : { + ...baseOptions, + provider: 'CoinbaseCDP' as const, + apiKeyId: connConfig.apiKeyId, + apiKeySecret: connConfig.apiKeySecret, + walletSecret: connConfig.walletSecret, + }; const connResult = await createConnector(connectorOptions); if (!connResult.ok) { @@ -438,10 +468,16 @@ export function AddPaymentFlow({ const msg = flow.connectorConfig ? `Payment manager "${mgrConfig.managerName}" and connector "${flow.connectorConfig.connectorName}" created` : `Payment manager "${mgrConfig.managerName}" created`; - setFlow({ name: 'success', message: msg }); + setFlow({ + name: 'success', + message: msg, + detail: + flow.connectorConfig?.provisionMode === 'QUICK_CREATE' + ? 'Run `agentcore deploy` to create the connector and receive its authorization URL.' + : undefined, + }); }; - // eslint-disable-next-line react-hooks/refs -- intentional: handler must close over current flow state confirmHandlerRef.current = () => void handleConfirmSubmit(); return ( @@ -489,7 +525,7 @@ export function AddPaymentFlow({ = { export type AddPaymentConnectorStep = | 'manager-select' - | 'provider-select' + | 'setup-select' // CoinbaseCDP credentials | 'api-key-id' | 'api-key-secret' @@ -62,6 +62,7 @@ export type AddPaymentConnectorStep = export interface AddPaymentConnectorConfig { managerName: string; + provisionMode: 'MANUAL' | 'QUICK_CREATE'; provider: PaymentProvider; // CoinbaseCDP apiKeyId: string; @@ -77,7 +78,7 @@ export interface AddPaymentConnectorConfig { export const CONNECTOR_STEP_LABELS: Record = { 'manager-select': 'Manager', - 'provider-select': 'Provider', + 'setup-select': 'Setup', 'api-key-id': 'API Key ID', 'api-key-secret': 'API Key Secret', 'wallet-secret': 'Wallet Secret', @@ -106,9 +107,24 @@ export const AUTH_TYPE_OPTIONS = [ }, ] as const; -export const PAYMENT_PROVIDER_OPTIONS = [ - { id: 'CoinbaseCDP' as const, title: 'Coinbase CDP', description: 'Coinbase Developer Platform wallet credentials' }, - { id: 'StripePrivy' as const, title: 'Stripe + Privy', description: 'Stripe payments via Privy embedded wallets' }, +export type PaymentConnectorSetup = 'quick-create' | 'coinbase-manual' | 'stripe-privy-manual'; + +export const PAYMENT_CONNECTOR_SETUP_OPTIONS = [ + { + id: 'quick-create' as const, + title: 'Quick Create with Coinbase', + description: 'Recommended - connect Coinbase after deployment', + }, + { + id: 'coinbase-manual' as const, + title: 'Coinbase CDP credentials', + description: 'Use existing Coinbase Developer Platform credentials', + }, + { + id: 'stripe-privy-manual' as const, + title: 'Stripe + Privy credentials', + description: 'Use existing Stripe and Privy credentials', + }, ] as const; /** Item ID for the auto payment toggle in the advanced config pane. */ diff --git a/src/cli/tui/screens/payment/useAddPaymentWizard.ts b/src/cli/tui/screens/payment/useAddPaymentWizard.ts index 7665d8018..f97aec46e 100644 --- a/src/cli/tui/screens/payment/useAddPaymentWizard.ts +++ b/src/cli/tui/screens/payment/useAddPaymentWizard.ts @@ -5,6 +5,7 @@ import type { AddPaymentConnectorStep, AddPaymentManagerConfig, AddPaymentManagerStep, + PaymentConnectorSetup, } from './types'; import { useCallback, useMemo, useState } from 'react'; @@ -168,17 +169,16 @@ const STRIPE_PRIVY_CREDENTIAL_STEPS: AddPaymentConnectorStep[] = [ 'authorization-id', ]; -function getConnectorStepsForProvider( +function getConnectorSteps( + provisionMode: 'MANUAL' | 'QUICK_CREATE', provider: PaymentProvider, needsManagerSelect: boolean ): AddPaymentConnectorStep[] { const steps: AddPaymentConnectorStep[] = []; if (needsManagerSelect) steps.push('manager-select'); - steps.push('provider-select'); - if (provider === 'StripePrivy') { - steps.push(...STRIPE_PRIVY_CREDENTIAL_STEPS); - } else { - steps.push(...CDP_CREDENTIAL_STEPS); + steps.push('setup-select'); + if (provisionMode !== 'QUICK_CREATE') { + steps.push(...(provider === 'StripePrivy' ? STRIPE_PRIVY_CREDENTIAL_STEPS : CDP_CREDENTIAL_STEPS)); } steps.push('connector-name', 'confirm'); return steps; @@ -187,6 +187,7 @@ function getConnectorStepsForProvider( function getDefaultConnectorConfig(preSelectedManager?: string): AddPaymentConnectorConfig { return { managerName: preSelectedManager ?? '', + provisionMode: 'QUICK_CREATE', provider: 'CoinbaseCDP', apiKeyId: '', apiKeySecret: '', @@ -204,8 +205,8 @@ export function useAddPaymentConnectorWizard(preSelectedManager?: string) { const [config, setConfig] = useState(() => getDefaultConnectorConfig(preSelectedManager)); const steps = useMemo( - () => getConnectorStepsForProvider(config.provider, needsManagerSelect), - [config.provider, needsManagerSelect] + () => getConnectorSteps(config.provisionMode, config.provider, needsManagerSelect), + [config.provisionMode, config.provider, needsManagerSelect] ); const [step, setStep] = useState(steps[0]!); @@ -233,13 +234,27 @@ export function useAddPaymentConnectorWizard(preSelectedManager?: string) { [advanceFrom] ); - const setProvider = useCallback((provider: PaymentProvider) => { - setConfig(c => ({ ...c, provider })); - // After selecting provider, advance to the first credential step - // The steps list will recompute via useMemo on next render - if (provider === 'StripePrivy') { + const setSetup = useCallback((setup: PaymentConnectorSetup) => { + if (setup === 'quick-create') { + setConfig(c => ({ + ...c, + provisionMode: 'QUICK_CREATE', + provider: 'CoinbaseCDP', + })); + setStep('connector-name'); + } else if (setup === 'stripe-privy-manual') { + setConfig(c => ({ + ...c, + provisionMode: 'MANUAL', + provider: 'StripePrivy', + })); setStep('app-id'); } else { + setConfig(c => ({ + ...c, + provisionMode: 'MANUAL', + provider: 'CoinbaseCDP', + })); setStep('api-key-id'); } }, []); @@ -322,7 +337,7 @@ export function useAddPaymentConnectorWizard(preSelectedManager?: string) { currentIndex, goBack, setManagerName, - setProvider, + setSetup, setApiKeyId, setApiKeySecret, setWalletSecret, diff --git a/src/cli/tui/screens/remove/RemoveScreen.tsx b/src/cli/tui/screens/remove/RemoveScreen.tsx index 21a646d79..e1f172921 100644 --- a/src/cli/tui/screens/remove/RemoveScreen.tsx +++ b/src/cli/tui/screens/remove/RemoveScreen.tsx @@ -30,7 +30,7 @@ const REMOVE_RESOURCES: { id: RemoveResourceType; title: string; description: st { id: 'online-eval', title: 'Online Eval Config', description: 'Remove an online eval config' }, { id: 'policy-engine', title: 'Policy Engine', description: 'Remove a policy engine' }, { id: 'policy', title: 'Policy', description: 'Remove a policy from a policy engine' }, - { id: 'payment', title: 'Payment [preview]', description: 'Remove a payment manager' }, + { id: 'payment', title: 'Payment', description: 'Remove a payment manager' }, { id: 'gateway', title: 'Gateway', description: 'Remove a gateway' }, { id: 'gateway-target', title: 'Gateway Target', description: 'Remove a gateway target' }, { diff --git a/src/schema/llm-compacted/agentcore.ts b/src/schema/llm-compacted/agentcore.ts index c2915ea8d..fc921afdc 100644 --- a/src/schema/llm-compacted/agentcore.ts +++ b/src/schema/llm-compacted/agentcore.ts @@ -604,8 +604,17 @@ interface PaymentManager { networkPreferences?: string[]; } -interface PaymentConnector { +type PaymentConnector = ManualPaymentConnector | QuickCreatePaymentConnector; + +interface ManualPaymentConnector { name: string; // @regex ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ @min 1 @max 48 provider: PaymentProvider; // default 'CoinbaseCDP' + provisionMode?: 'MANUAL'; credentialName: string; } + +interface QuickCreatePaymentConnector { + name: string; // @regex ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ @min 1 @max 48 + provider: 'CoinbaseCDP'; + provisionMode: 'QUICK_CREATE'; +} diff --git a/src/schema/schemas/__tests__/deployed-state.test.ts b/src/schema/schemas/__tests__/deployed-state.test.ts index 97da30a4a..af5b7044b 100644 --- a/src/schema/schemas/__tests__/deployed-state.test.ts +++ b/src/schema/schemas/__tests__/deployed-state.test.ts @@ -11,6 +11,7 @@ import { McpLambdaDeployedStateSchema, McpRuntimeDeployedStateSchema, MemoryDeployedStateSchema, + PaymentConnectorDeployedStateSchema, VpcConfigSchema, createValidatedDeployedStateSchema, } from '../deployed-state.js'; @@ -256,6 +257,35 @@ describe('CredentialDeployedStateSchema', () => { }); }); +describe('PaymentConnectorDeployedStateSchema', () => { + it.each([ + ['legacy manual state', { connectorId: 'connector-123', credentialProviderArn: 'arn:credential-provider' }, true], + [ + 'explicit manual state', + { + connectorId: 'connector-123', + provisionMode: 'MANUAL', + credentialProviderArn: 'arn:credential-provider', + }, + true, + ], + ['Quick Create state', { connectorId: 'connector-123', provisionMode: 'QUICK_CREATE' }, true], + ['legacy manual state without an ARN', { connectorId: 'connector-123' }, false], + ['explicit manual state without an ARN', { connectorId: 'connector-123', provisionMode: 'MANUAL' }, false], + [ + 'Quick Create state with an ARN', + { + connectorId: 'connector-123', + provisionMode: 'QUICK_CREATE', + credentialProviderArn: 'arn:credential-provider', + }, + false, + ], + ])('%s parses as expected', (_name, state, expected) => { + expect(PaymentConnectorDeployedStateSchema.safeParse(state).success).toBe(expected); + }); +}); + describe('DeployedResourceStateSchema', () => { it('accepts empty resource state', () => { expect(DeployedResourceStateSchema.safeParse({}).success).toBe(true); diff --git a/src/schema/schemas/__tests__/payment.test.ts b/src/schema/schemas/__tests__/payment.test.ts index c4938b0f6..deab0dab4 100644 --- a/src/schema/schemas/__tests__/payment.test.ts +++ b/src/schema/schemas/__tests__/payment.test.ts @@ -1,4 +1,9 @@ -import { PaymentConnectorNameSchema, PaymentManagerNameSchema, PaymentManagerSchema } from '../primitives/payment'; +import { + PaymentConnectorNameSchema, + PaymentConnectorSchema, + PaymentManagerNameSchema, + PaymentManagerSchema, +} from '../primitives/payment'; import { describe, expect, it } from 'vitest'; describe('PaymentManagerNameSchema', () => { @@ -75,6 +80,64 @@ describe('PaymentConnectorNameSchema', () => { }); }); +describe('PaymentConnectorSchema', () => { + it('preserves existing manual connectors without materializing provisionMode', () => { + const result = PaymentConnectorSchema.parse({ + name: 'ManualConnector', + provider: 'CoinbaseCDP', + credentialName: 'manual-credential', + }); + + expect(result).toEqual({ + name: 'ManualConnector', + provider: 'CoinbaseCDP', + credentialName: 'manual-credential', + }); + expect('provisionMode' in result).toBe(false); + }); + + it('accepts a Coinbase Quick Create connector without credentialName', () => { + const result = PaymentConnectorSchema.safeParse({ + name: 'QuickConnector', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + }); + + expect(result.success).toBe(true); + }); + + it('rejects credentialName for Quick Create', () => { + const result = PaymentConnectorSchema.safeParse({ + name: 'QuickConnector', + provider: 'CoinbaseCDP', + provisionMode: 'QUICK_CREATE', + credentialName: 'not-allowed', + }); + + expect(result.success).toBe(false); + }); + + it('rejects StripePrivy for Quick Create', () => { + const result = PaymentConnectorSchema.safeParse({ + name: 'QuickConnector', + provider: 'StripePrivy', + provisionMode: 'QUICK_CREATE', + }); + + expect(result.success).toBe(false); + }); + + it('requires credentialName for explicit manual mode', () => { + const result = PaymentConnectorSchema.safeParse({ + name: 'ManualConnector', + provider: 'CoinbaseCDP', + provisionMode: 'MANUAL', + }); + + expect(result.success).toBe(false); + }); +}); + describe('PaymentManagerSchema', () => { const validBase = { name: 'testManager', connectors: [] }; diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index c1eaa46fd..1076b3ff7 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -38,6 +38,7 @@ import { PaymentManagerNameSchema, PaymentManagerSchema, PaymentProviderSchema, + PaymentProvisionModeSchema, } from './primitives/payment'; import { PolicyEngineSchema } from './primitives/policy'; import { TagsSchema } from './primitives/tags'; @@ -140,9 +141,16 @@ export { PaymentConnectorSchema, PaymentConnectorNameSchema, PaymentProviderSchema, + PaymentProvisionModeSchema, PaymentAuthorizerTypeSchema, }; -export type { PaymentManager, PaymentConnector, PaymentProvider, PaymentAuthorizerType } from './primitives/payment'; +export type { + PaymentManager, + PaymentConnector, + PaymentProvider, + PaymentProvisionMode, + PaymentAuthorizerType, +} from './primitives/payment'; // ============================================================================ // ManagedBy Schema @@ -705,6 +713,7 @@ export const AgentCoreProjectSpecSchema = z const paymentIndex = (spec.payments ?? []).indexOf(payment); for (const connector of payment.connectors) { const connectorIndex = payment.connectors.indexOf(connector); + if (connector.provisionMode === 'QUICK_CREATE') continue; const credential = spec.credentials.find(c => c.name === connector.credentialName); if (!credential) { ctx.addIssue({ diff --git a/src/schema/schemas/deployed-state.ts b/src/schema/schemas/deployed-state.ts index 49d369da2..34fa525dd 100644 --- a/src/schema/schemas/deployed-state.ts +++ b/src/schema/schemas/deployed-state.ts @@ -288,12 +288,22 @@ export type RuntimeEndpointDeployedState = z.infer; // ============================================================================ diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index 962989e54..1cb430c1a 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -111,7 +111,13 @@ export { ManagedMemoryStrategySchema, } from './harness'; -export type { PaymentManager, PaymentConnector, PaymentProvider, PaymentAuthorizerType } from './payment'; +export type { + PaymentManager, + PaymentConnector, + PaymentProvider, + PaymentProvisionMode, + PaymentAuthorizerType, +} from './payment'; export { DEFAULT_AUTO_PAYMENT, DEFAULT_SPEND_LIMIT, @@ -119,6 +125,9 @@ export { PaymentManagerNameSchema, PaymentConnectorSchema, PaymentConnectorNameSchema, + ManualPaymentConnectorSchema, + QuickCreatePaymentConnectorSchema, + PaymentProvisionModeSchema, PaymentProviderSchema, PaymentAuthorizerTypeSchema, } from './payment'; diff --git a/src/schema/schemas/primitives/payment.ts b/src/schema/schemas/primitives/payment.ts index 8200d71b4..af81d11f1 100644 --- a/src/schema/schemas/primitives/payment.ts +++ b/src/schema/schemas/primitives/payment.ts @@ -47,12 +47,25 @@ export const PaymentConnectorNameSchema = z // Payment Connector Schema // ============================================================================ -export const PaymentConnectorSchema = z.object({ +export const PaymentProvisionModeSchema = z.enum(['MANUAL', 'QUICK_CREATE']); +export type PaymentProvisionMode = z.infer; + +export const ManualPaymentConnectorSchema = z.object({ name: PaymentConnectorNameSchema, provider: PaymentProviderSchema.default('CoinbaseCDP'), + provisionMode: z.literal('MANUAL').optional(), credentialName: z.string().min(1), }); +export const QuickCreatePaymentConnectorSchema = z.object({ + name: PaymentConnectorNameSchema, + provider: z.literal('CoinbaseCDP'), + provisionMode: z.literal('QUICK_CREATE'), + credentialName: z.never().optional(), +}); + +export const PaymentConnectorSchema = z.union([QuickCreatePaymentConnectorSchema, ManualPaymentConnectorSchema]); + export type PaymentConnector = z.infer; // ============================================================================