diff --git a/README.md b/README.md index 79822cc32..361b1aa72 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,15 @@ agentcore # interactive TUI │ │ ├── list # list API key credential providers │ │ ├── update # update an API key credential provider │ │ └── delete # delete an API key credential provider -│ └── oauth2-credential-provider -│ ├── create # create an OAuth2 credential provider -│ ├── get # get an OAuth2 credential provider -│ ├── list # list OAuth2 credential providers -│ ├── update # update an OAuth2 credential provider -│ └── delete # delete an OAuth2 credential provider +│ ├── oauth2-credential-provider +│ │ ├── create # create an OAuth2 credential provider +│ │ ├── get # get an OAuth2 credential provider +│ │ ├── list # list OAuth2 credential providers +│ │ ├── update # update an OAuth2 credential provider +│ │ └── delete # delete an OAuth2 credential provider +│ └── payment-credential-provider +│ ├── get # get a payment credential provider +│ └── list # list payment credential providers ├── runtime # inspect deployed AgentCore Runtimes │ ├── get # fetch a Runtime by id │ ├── list # list Runtimes (server-side paginated) @@ -107,6 +110,20 @@ agentcore # interactive TUI │ │ └── list # list Rules under a Gateway │ └── policy │ └── generate # generate Cedar for a Gateway from a prompt (TUI when run bare) +├── payment # inspect AgentCore Payments (command line only for now) +│ ├── manager +│ │ ├── get # get a payment manager by id +│ │ └── list # list payment managers (server-side paginated) +│ ├── connector # connectors under a payment manager +│ │ ├── get # get a connector (shows the Quick Create authorization URL while pending) +│ │ └── list # list a manager's connectors +│ ├── session # budget-limited payment contexts (data plane) +│ │ ├── get +│ │ └── list +│ └── instrument # embedded crypto wallets (data plane) +│ ├── get +│ ├── list +│ └── balance # read token balance on an explicit chain (default token: USDC) ├── eval # evaluate and optimize AgentCore agents │ └── evaluator # manage AgentCore evaluators │ ├── llm-as-a-judge # LLM-as-a-Judge evaluators @@ -186,6 +203,69 @@ agentcore project invoke harness \ Use `--target` to select a deployment target. When a project declares exactly one resource of the requested type, `--name` may be omitted. +### Inspect AgentCore Payments + +The `payment` commands call the Payments control and data planes directly, with +no project involved. This command family currently provides read-only inspection +of existing managers, connectors, sessions, instruments, and payment credential +providers. It does not create IAM roles or change provider credentials. + +Choose a manager from `manager list` and use its `paymentManagerId` below: + +```bash +agentcore payment manager list --json +MANAGER_ID='' +agentcore payment manager get --id "$MANAGER_ID" +agentcore payment connector list --manager-id "$MANAGER_ID" +``` + +`--user-id` is the application user ID used when the session or instrument was +created, not an IAM username or AWS profile. Session and instrument reads require +it with IAM authentication; their lists return that user's resources, not every +user's resources under the manager. + +```bash +USER_ID='alice' # Use the application user ID associated with the resources. +agentcore payment session list --manager-id "$MANAGER_ID" --user-id "$USER_ID" +agentcore payment instrument list --manager-id "$MANAGER_ID" --user-id "$USER_ID" + +# Use paymentInstrumentId and paymentConnectorId from the same instrument list item. +INSTRUMENT_ID='' +CONNECTOR_ID='' +agentcore payment instrument get --manager-id "$MANAGER_ID" \ + --instrument-id "$INSTRUMENT_ID" --user-id "$USER_ID" +agentcore payment instrument balance --manager-id "$MANAGER_ID" \ + --connector-id "$CONNECTOR_ID" --instrument-id "$INSTRUMENT_ID" \ + --user-id "$USER_ID" --chain BASE_SEPOLIA +``` + +To inspect connector or credential provider metadata: + +```bash +agentcore payment connector get --manager-id "$MANAGER_ID" --connector-id "$CONNECTOR_ID" +agentcore identity payment-credential-provider list --json +agentcore identity payment-credential-provider get --name '' +``` + +The optional `--agent-name` on session and instrument reads labels the request for +observability. It does not select an AgentCore agent or filter the results. + +`instrument get` returns instrument metadata without querying balances. `balance` +requires an explicit chain and defaults to `--token USDC`; wallet network families +such as ETHEREUM do not identify whether to query mainnet or a testnet. The JSON +response retains the raw atomic amount string and decimals. A service error is +reported as an error, never converted to a zero balance. + +Data-plane commands work against managers that use the `AWS_IAM` authorizer. +The CLI resolves `--manager-id` through `GetPaymentManager` in the configured +region, then supplies the returned ARN to the data-plane API. Callers need +`bedrock-agentcore:GetPaymentManager` as well as the relevant data-plane action. +Region resolution follows the other imperative commands: `--region`, environment +variables, the active AWS profile, then the CLI default. +A `CUSTOM_JWT` manager accepts only bearer tokens on its data plane, which +these commands do not send yet; the CLI reports that limitation before calling +the data plane. + ### Examples ```bash diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index cb231cc2b..3535ace9d 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -37,7 +37,7 @@ describe("menus list command-line-only subcommands below a divider", () => { await waitForText(r.lastFrame, "command line only"); expect(menuEntries(r.lastFrame()!)).toEqual({ screens: ["project", "harness", "identity", "runtime", "memory", "gateway", "eval"], - cliOnly: ["feedback", "config", "update"], + cliOnly: ["payment", "feedback", "config", "update"], }); r.unmount(); }); diff --git a/src/core/identity.tsx b/src/core/identity.tsx index 9d8070451..4ca0e69b5 100644 --- a/src/core/identity.tsx +++ b/src/core/identity.tsx @@ -10,6 +10,7 @@ import { GetPaymentCredentialProviderCommand, ListApiKeyCredentialProvidersCommand, ListOauth2CredentialProvidersCommand, + ListPaymentCredentialProvidersCommand, UpdateApiKeyCredentialProviderCommand, UpdateOauth2CredentialProviderCommand, UpdatePaymentCredentialProviderCommand, @@ -26,6 +27,7 @@ import { type CreatePaymentCredentialProviderResponse, type DeletePaymentCredentialProviderResponse, type GetPaymentCredentialProviderResponse, + type ListPaymentCredentialProvidersResponse, type UpdatePaymentCredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { @@ -155,6 +157,16 @@ export class IdentityClient implements CoreIdentityClient { .send(new GetPaymentCredentialProviderCommand({ name })); } + async listPaymentCredentialProviders( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListPaymentCredentialProvidersCommand({ nextToken, maxResults })); + } + async updatePaymentCredentialProvider( input: UpdatePaymentCredentialProviderInput, options: CoreOptions, diff --git a/src/core/index.tsx b/src/core/index.tsx index d6c8658b0..bff250bd0 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -7,6 +7,7 @@ import { GatewayClient } from "./gateway"; import { HarnessClient } from "./harness"; import { IdentityClient } from "./identity"; import { MemoryClient } from "./memory"; +import { PaymentClient } from "./payment"; import { PolicyClient } from "./policy"; import { ObservabilityClient } from "./observability"; import { CloudWatchClient } from "./observability/index"; @@ -79,6 +80,7 @@ export class CoreClient implements AwsClients { readonly eval: EvalClient; readonly observability: ObservabilityClient; readonly policy: PolicyClient; + readonly payment: PaymentClient; readonly projectManager: ProjectManager; readonly bedrockAgentImporter: CoreBedrockAgentImporter; @@ -101,6 +103,7 @@ export class CoreClient implements AwsClients { ); this.gateway = new GatewayClient(this, fetch, this.logger.child({ module: "gateway" })); this.policy = new PolicyClient(this, this.logger.child({ module: "policy" })); + this.payment = new PaymentClient(this); // EvalClient shares the injected fetch: dataset content is served from a // presigned S3 URL, outside the SDK seam the other operations use. The logger // is used for batch-evaluation result-log diagnostics. diff --git a/src/core/payment.read.test.ts b/src/core/payment.read.test.ts new file mode 100644 index 000000000..66ad240fa --- /dev/null +++ b/src/core/payment.read.test.ts @@ -0,0 +1,111 @@ +import { expect, mock, test } from "bun:test"; +import { GetPaymentManagerCommand } from "@aws-sdk/client-bedrock-agentcore-control"; +import { ListPaymentSessionsCommand } from "@aws-sdk/client-bedrock-agentcore"; +import { PaymentClient } from "./payment"; +import type { AwsClients, ClientConfig } from "./types"; + +const MANAGER_ID = "checkout-abc1234567"; +const MANAGER_ARN = `arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/${MANAGER_ID}`; +const request = { managerId: MANAGER_ID, userId: "alice" }; +const options = { + region: "us-east-1", + endpointUrl: "https://payments.example.test", + credentials: { accessKeyId: "test-key", secretAccessKey: "test-secret" }, +}; + +function setup() { + const controlSend = mock(async (_command: unknown) => ({ + paymentManagerArn: MANAGER_ARN, + authorizerType: "AWS_IAM", + })); + const response = { paymentSessions: [] }; + const dataSend = mock(async (_command: unknown) => response); + const control = mock( + (_config: ClientConfig) => + ({ send: controlSend }) as unknown as ReturnType, + ); + const data = mock( + (_config: ClientConfig) => ({ send: dataSend }) as unknown as ReturnType, + ); + return { + client: new PaymentClient({ control, data }), + control, + data, + controlSend, + dataSend, + response, + }; +} + +test("resolves the manager in the configured region and forwards the returned ARN and context", async () => { + const { client, control, data, controlSend, dataSend, response } = setup(); + const input = { ...request, agentName: "agent", nextToken: "page+2/=", maxResults: 5 }; + expect(await client.listPaymentSessions(input, options)).toBe(response); + expect(controlSend.mock.calls[0]?.[0]).toBeInstanceOf(GetPaymentManagerCommand); + expect(controlSend.mock.calls[0]?.[0]).toMatchObject({ input: { paymentManagerId: MANAGER_ID } }); + expect(dataSend.mock.calls[0]?.[0]).toBeInstanceOf(ListPaymentSessionsCommand); + expect((dataSend.mock.calls[0]![0] as ListPaymentSessionsCommand).input).toEqual({ + paymentManagerArn: MANAGER_ARN, + userId: "alice", + agentName: "agent", + nextToken: "page+2/=", + maxResults: 5, + }); + const config = { + region: options.region, + endpoint: options.endpointUrl, + credentials: options.credentials, + }; + expect(control).toHaveBeenCalledWith(config); + expect(data).toHaveBeenCalledWith(config); + + controlSend.mockResolvedValueOnce({ + paymentManagerArn: `${MANAGER_ARN}-new`, + authorizerType: "AWS_IAM", + }); + await client.listPaymentSessions(request, options); + expect(controlSend).toHaveBeenCalledTimes(2); + expect(dataSend.mock.calls[1]?.[0]).toMatchObject({ + input: { paymentManagerArn: `${MANAGER_ARN}-new`, userId: "alice" }, + }); + expect(input).toEqual({ ...request, agentName: "agent", nextToken: "page+2/=", maxResults: 5 }); +}); + +test("rejects an ARN selector before contacting AWS", async () => { + const { client, control, data } = setup(); + await expect(client.listPaymentSessions({ managerId: MANAGER_ARN }, options)).rejects.toThrow( + "use a payment manager ID, not an ARN", + ); + expect(control).not.toHaveBeenCalled(); + expect(data).not.toHaveBeenCalled(); +}); + +test.each([ + [{ paymentManagerArn: MANAGER_ARN, authorizerType: "CUSTOM_JWT" }, "CUSTOM_JWT"], + [{ paymentManagerArn: "", authorizerType: "AWS_IAM" }, "returned no ARN"], +] as const)( + "rejects an unusable manager before data-plane access: %j", + async (manager, message) => { + const { client, controlSend, data } = setup(); + controlSend.mockResolvedValueOnce(manager); + await expect(client.listPaymentSessions(request, options)).rejects.toThrow(message); + expect(data).not.toHaveBeenCalled(); + }, +); + +test("preserves a lookup failure without contacting the data plane", async () => { + const { client, controlSend, data } = setup(); + const error = new Error("Payment manager not found"); + controlSend.mockRejectedValueOnce(error); + await expect(client.listPaymentSessions(request, options)).rejects.toBe(error); + expect(data).not.toHaveBeenCalled(); +}); + +test("preserves a data-plane failure without retrying the lookup", async () => { + const { client, controlSend, dataSend } = setup(); + const error = new Error("Access denied"); + dataSend.mockRejectedValueOnce(error); + await expect(client.listPaymentSessions(request, options)).rejects.toBe(error); + expect(controlSend).toHaveBeenCalledTimes(1); + expect(dataSend).toHaveBeenCalledTimes(1); +}); diff --git a/src/core/payment.tsx b/src/core/payment.tsx new file mode 100644 index 000000000..58a4bc72d --- /dev/null +++ b/src/core/payment.tsx @@ -0,0 +1,165 @@ +import { + GetPaymentConnectorCommand, + GetPaymentManagerCommand, + ListPaymentConnectorsCommand, + ListPaymentManagersCommand, + type GetPaymentConnectorResponse, + type GetPaymentManagerResponse, + type ListPaymentConnectorsResponse, + type ListPaymentManagersResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { + GetPaymentInstrumentBalanceCommand, + GetPaymentInstrumentCommand, + GetPaymentSessionCommand, + ListPaymentInstrumentsCommand, + ListPaymentSessionsCommand, + type BedrockAgentCoreClient, + type GetPaymentInstrumentResponse, + type GetPaymentInstrumentBalanceResponse, + type GetPaymentSessionResponse, + type ListPaymentInstrumentsResponse, + type ListPaymentSessionsResponse, +} from "@aws-sdk/client-bedrock-agentcore"; +import { InputValidationError, MalformedServiceResponseError } from "../errors"; +import type { + CorePaymentClient, + GetPaymentSessionInput, + ListPaymentSessionsInput, + GetPaymentInstrumentInput, + GetPaymentInstrumentBalanceInput, + ListPaymentInstrumentsInput, +} from "../handlers/payment/types"; +import type { AwsClients, CoreOptions } from "./types"; +import { toClientConfig } from "./utils"; + +// PaymentClient implements the payment-facing operations on top of the shared +// AWS clients provided by CoreClient. Managers and connectors live on the control +// plane; sessions and instruments on the data plane. +export class PaymentClient implements CorePaymentClient { + constructor(private readonly clients: Pick) {} + + // ─── payment managers ─────────────────────────────────────────────────────── + + async getPaymentManager(id: string, options: CoreOptions): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new GetPaymentManagerCommand({ paymentManagerId: id })); + } + + async listPaymentManagers( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListPaymentManagersCommand({ nextToken, maxResults })); + } + + // ─── payment connectors ───────────────────────────────────────────────────── + + async getPaymentConnector( + managerId: string, + connectorId: string, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send( + new GetPaymentConnectorCommand({ + paymentManagerId: managerId, + paymentConnectorId: connectorId, + }), + ); + } + + async listPaymentConnectors( + managerId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send( + new ListPaymentConnectorsCommand({ paymentManagerId: managerId, nextToken, maxResults }), + ); + } + + // ─── payment sessions (data plane) ────────────────────────────────────────── + + async getPaymentSession( + input: GetPaymentSessionInput, + options: CoreOptions, + ): Promise { + const { managerId, ...request } = input; + return this.withPaymentManagerArn(managerId, options, (data, paymentManagerArn) => + data.send(new GetPaymentSessionCommand({ paymentManagerArn, ...request })), + ); + } + + async listPaymentSessions( + input: ListPaymentSessionsInput, + options: CoreOptions, + ): Promise { + const { managerId, ...request } = input; + return this.withPaymentManagerArn(managerId, options, (data, paymentManagerArn) => + data.send(new ListPaymentSessionsCommand({ paymentManagerArn, ...request })), + ); + } + + // ─── payment instruments (data plane) ─────────────────────────────────────── + + async getPaymentInstrument( + input: GetPaymentInstrumentInput, + options: CoreOptions, + ): Promise { + const { managerId, ...request } = input; + return this.withPaymentManagerArn(managerId, options, (data, paymentManagerArn) => + data.send(new GetPaymentInstrumentCommand({ paymentManagerArn, ...request })), + ); + } + + async getPaymentInstrumentBalance( + input: GetPaymentInstrumentBalanceInput, + options: CoreOptions, + ): Promise { + const { managerId, ...request } = input; + return this.withPaymentManagerArn(managerId, options, (data, paymentManagerArn) => + data.send(new GetPaymentInstrumentBalanceCommand({ paymentManagerArn, ...request })), + ); + } + + async listPaymentInstruments( + input: ListPaymentInstrumentsInput, + options: CoreOptions, + ): Promise { + const { managerId, ...request } = input; + return this.withPaymentManagerArn(managerId, options, (data, paymentManagerArn) => + data.send(new ListPaymentInstrumentsCommand({ paymentManagerArn, ...request })), + ); + } + + // ─── helpers ──────────────────────────────────────────────────────────────── + + private async withPaymentManagerArn( + managerId: string, + options: CoreOptions, + send: (data: BedrockAgentCoreClient, paymentManagerArn: string) => Promise, + ): Promise { + if (managerId.startsWith("arn:")) { + throw new InputValidationError("use a payment manager ID, not an ARN"); + } + const manager = await this.getPaymentManager(managerId, options); + if (manager.authorizerType === "CUSTOM_JWT") { + throw new InputValidationError( + `payment manager "${managerId}" uses the CUSTOM_JWT authorizer, so its data plane accepts only bearer tokens; ` + + "this CLI does not support bearer tokens for payment commands yet. " + + "Use an AWS_IAM payment manager, or call the API directly with a JWT.", + ); + } + if (!manager.paymentManagerArn) { + throw new MalformedServiceResponseError(`payment manager "${managerId}" returned no ARN`); + } + return send(this.clients.data(toClientConfig(options)), manager.paymentManagerArn); + } +} diff --git a/src/handlers/identity/identity.test.tsx b/src/handlers/identity/identity.test.tsx index 9537f597e..641607d9b 100644 --- a/src/handlers/identity/identity.test.tsx +++ b/src/handlers/identity/identity.test.tsx @@ -56,6 +56,7 @@ describe("identity command hierarchy", () => { expect(identity?.children().map((child) => child.name())).toEqual([ "api-key-credential-provider", "oauth2-credential-provider", + "payment-credential-provider", ]); expect( identity diff --git a/src/handlers/identity/index.tsx b/src/handlers/identity/index.tsx index 64011df40..68d4dcd41 100644 --- a/src/handlers/identity/index.tsx +++ b/src/handlers/identity/index.tsx @@ -5,11 +5,13 @@ import type { AppIO } from "../../io"; import type { Core } from "../types"; import { createApiKeyCredentialProviderHandler } from "./api-key-credential-provider"; import { createOauth2CredentialProviderHandler } from "./oauth2-credential-provider"; +import { createPaymentCredentialProviderHandler } from "./payment-credential-provider"; export function createIdentityHandler(core: Core, io: AppIO): Router { return new Router("identity", "manage AgentCore Identity resources") .use(withTuiOnEmptyFlagsAndArgs(core, io)) .default(renderTui(core, io)) .handler(createApiKeyCredentialProviderHandler(core, io)) - .handler(createOauth2CredentialProviderHandler(core, io)); + .handler(createOauth2CredentialProviderHandler(core, io)) + .handler(createPaymentCredentialProviderHandler(core, io)); } diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.9ebdd66cb38819e.json b/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.9ebdd66cb38819e.json new file mode 100644 index 000000000..f636ebf3a --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.9ebdd66cb38819e.json @@ -0,0 +1,24 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + }, + "createdTime": { + "$date": "2026-09-08T20:16:25.544Z" + }, + "lastUpdatedTime": { + "$date": "2026-09-08T20:16:25.544Z" + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.f7b7a089d09d9e1d.json b/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.f7b7a089d09d9e1d.json new file mode 100644 index 000000000..34b0fd4c5 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/GetPaymentCredentialProviderCommand.f7b7a089d09d9e1d.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "PaymentCredentialProvider not found" + } +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.23f97c9dcdd6350b.json b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.23f97c9dcdd6350b.json new file mode 100644 index 000000000..93aa78839 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.23f97c9dcdd6350b.json @@ -0,0 +1,114 @@ +{ + "credentialProviders": [ + { + "name": "DeployTest-CdpConn-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/DeployTest-CdpConn-cdp", + "createdTime": { + "$date": "2026-05-14T15:48:17.857Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-14T15:48:17.857Z" + } + }, + { + "name": "InvokeMgr-InvokeCdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/InvokeMgr-InvokeCdp-cdp", + "createdTime": { + "$date": "2026-05-20T19:09:36.566Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-20T19:09:36.566Z" + } + }, + { + "name": "MyPaymentManager-MyCdpConnector-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManager-MyCdpConnector-cdp", + "createdTime": { + "$date": "2026-05-05T03:02:59.583Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-21T19:22:56.053Z" + } + }, + { + "name": "MyPaymentManager-MyStripePrivyConnector-stripe-privy", + "credentialProviderVendor": "StripePrivy", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManager-MyStripePrivyConnector-stripe-privy", + "createdTime": { + "$date": "2026-05-21T19:22:56.308Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-21T19:22:56.308Z" + } + }, + { + "name": "MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp", + "createdTime": { + "$date": "2026-06-08T18:10:19.508Z" + }, + "lastUpdatedTime": { + "$date": "2026-06-08T18:10:19.508Z" + } + }, + { + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "createdTime": { + "$date": "2026-09-08T20:16:25.544Z" + }, + "lastUpdatedTime": { + "$date": "2026-09-08T20:16:25.544Z" + } + }, + { + "name": "agentcore-cli-payment-fixture-2", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture-2", + "createdTime": { + "$date": "2026-09-08T20:16:26.070Z" + }, + "lastUpdatedTime": { + "$date": "2026-09-08T20:16:26.070Z" + } + }, + { + "name": "cdp-creds", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/cdp-creds", + "createdTime": { + "$date": "2026-05-12T21:18:46.009Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-12T21:33:40.066Z" + } + }, + { + "name": "paymgr-cdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/paymgr-cdp-cdp", + "createdTime": { + "$date": "2026-05-21T21:19:41.791Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-22T04:40:50.491Z" + } + }, + { + "name": "pmgr-cdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/pmgr-cdp-cdp", + "createdTime": { + "$date": "2026-05-22T01:59:54.168Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-22T02:42:24.884Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.52636e8890926ea8.json b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.52636e8890926ea8.json new file mode 100644 index 000000000..754a56eac --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.52636e8890926ea8.json @@ -0,0 +1,16 @@ +{ + "credentialProviders": [ + { + "name": "InvokeMgr-InvokeCdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/InvokeMgr-InvokeCdp-cdp", + "createdTime": { + "$date": "2026-05-20T19:09:36.566Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-20T19:09:36.566Z" + } + } + ], + "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjAzMTQxMDQxOTQ3I1BBWU1FTlQiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6Ikludm9rZU1nci1JbnZva2VDZHAtY2RwIiwiYXR0cmlidXRlVHlwZSI6IlMifSwiUEsiOnsidmFsdWVTdHIiOiJQQVlNRU5UX0NSRURfUFJPVklERVIjNjAzMTQxMDQxOTQ3I2RlZmF1bHQjSW52b2tlTWdyLUludm9rZUNkcC1jZHAiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9fQ==" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.7d2e22c637f6b633.json b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.7d2e22c637f6b633.json new file mode 100644 index 000000000..b26d58bc0 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/ListPaymentCredentialProvidersCommand.7d2e22c637f6b633.json @@ -0,0 +1,16 @@ +{ + "credentialProviders": [ + { + "name": "DeployTest-CdpConn-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/DeployTest-CdpConn-cdp", + "createdTime": { + "$date": "2026-05-14T15:48:17.857Z" + }, + "lastUpdatedTime": { + "$date": "2026-05-14T15:48:17.857Z" + } + } + ], + "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjAzMTQxMDQxOTQ3I1BBWU1FTlQiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6IkRlcGxveVRlc3QtQ2RwQ29ubi1jZHAiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJQSyI6eyJ2YWx1ZVN0ciI6IlBBWU1FTlRfQ1JFRF9QUk9WSURFUiM2MDMxNDEwNDE5NDcjZGVmYXVsdCNEZXBsb3lUZXN0LUNkcENvbm4tY2RwIiwiYXR0cmlidXRlVHlwZSI6IlMifX0=" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/get.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/get.golden.json new file mode 100644 index 000000000..db9929018 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/get.golden.json @@ -0,0 +1,20 @@ +{ + "name": "agentcore-cli-payment-fixture", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "providerConfigurationOutput": { + "coinbaseCdpConfiguration": { + "apiKeyId": "agentcore-cli-fixture-key", + "apiKeySecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/apikey-eNjrnd" + }, + "walletSecretArn": { + "secretArn": "arn:aws:secretsmanager:us-west-2:603141041947:secret:bedrock-agentcore-identity!default/payment/coinbasecdp/agentcore-cli-payment-fixture-541ff54c/wallet-DFZFS6" + }, + "apiKeySecretSource": "MANAGED", + "walletSecretSource": "MANAGED" + } + }, + "createdTime": "2026-09-08T20:16:25.544Z", + "lastUpdatedTime": "2026-09-08T20:16:25.544Z" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-1.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-1.golden.json new file mode 100644 index 000000000..e9db4018d --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-1.golden.json @@ -0,0 +1,12 @@ +{ + "credentialProviders": [ + { + "name": "DeployTest-CdpConn-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/DeployTest-CdpConn-cdp", + "createdTime": "2026-05-14T15:48:17.857Z", + "lastUpdatedTime": "2026-05-14T15:48:17.857Z" + } + ], + "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjAzMTQxMDQxOTQ3I1BBWU1FTlQiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6IkRlcGxveVRlc3QtQ2RwQ29ubi1jZHAiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJQSyI6eyJ2YWx1ZVN0ciI6IlBBWU1FTlRfQ1JFRF9QUk9WSURFUiM2MDMxNDEwNDE5NDcjZGVmYXVsdCNEZXBsb3lUZXN0LUNkcENvbm4tY2RwIiwiYXR0cmlidXRlVHlwZSI6IlMifX0=" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-2.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-2.golden.json new file mode 100644 index 000000000..06cf4238d --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/list-page-2.golden.json @@ -0,0 +1,12 @@ +{ + "credentialProviders": [ + { + "name": "InvokeMgr-InvokeCdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/InvokeMgr-InvokeCdp-cdp", + "createdTime": "2026-05-20T19:09:36.566Z", + "lastUpdatedTime": "2026-05-20T19:09:36.566Z" + } + ], + "nextToken": "eyJBY2NvdW50QW5kVHlwZUdzaVBrIjp7InZhbHVlU3RyIjoiNjAzMTQxMDQxOTQ3I1BBWU1FTlQiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9LCJTSyI6eyJ2YWx1ZVN0ciI6Ikludm9rZU1nci1JbnZva2VDZHAtY2RwIiwiYXR0cmlidXRlVHlwZSI6IlMifSwiUEsiOnsidmFsdWVTdHIiOiJQQVlNRU5UX0NSRURfUFJPVklERVIjNjAzMTQxMDQxOTQ3I2RlZmF1bHQjSW52b2tlTWdyLUludm9rZUNkcC1jZHAiLCJhdHRyaWJ1dGVUeXBlIjoiUyJ9fQ==" +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/__fixtures__/list.golden.json b/src/handlers/identity/payment-credential-provider/__fixtures__/list.golden.json new file mode 100644 index 000000000..86a578c46 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/__fixtures__/list.golden.json @@ -0,0 +1,74 @@ +{ + "credentialProviders": [ + { + "name": "DeployTest-CdpConn-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/DeployTest-CdpConn-cdp", + "createdTime": "2026-05-14T15:48:17.857Z", + "lastUpdatedTime": "2026-05-14T15:48:17.857Z" + }, + { + "name": "InvokeMgr-InvokeCdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/InvokeMgr-InvokeCdp-cdp", + "createdTime": "2026-05-20T19:09:36.566Z", + "lastUpdatedTime": "2026-05-20T19:09:36.566Z" + }, + { + "name": "MyPaymentManager-MyCdpConnector-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManager-MyCdpConnector-cdp", + "createdTime": "2026-05-05T03:02:59.583Z", + "lastUpdatedTime": "2026-05-21T19:22:56.053Z" + }, + { + "name": "MyPaymentManager-MyStripePrivyConnector-stripe-privy", + "credentialProviderVendor": "StripePrivy", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManager-MyStripePrivyConnector-stripe-privy", + "createdTime": "2026-05-21T19:22:56.308Z", + "lastUpdatedTime": "2026-05-21T19:22:56.308Z" + }, + { + "name": "MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp", + "createdTime": "2026-06-08T18:10:19.508Z", + "lastUpdatedTime": "2026-06-08T18:10:19.508Z" + }, + { + "name": "agentcore-cli-payment-fixture", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture", + "createdTime": "2026-09-08T20:16:25.544Z", + "lastUpdatedTime": "2026-09-08T20:16:25.544Z" + }, + { + "name": "agentcore-cli-payment-fixture-2", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/agentcore-cli-payment-fixture-2", + "createdTime": "2026-09-08T20:16:26.070Z", + "lastUpdatedTime": "2026-09-08T20:16:26.070Z" + }, + { + "name": "cdp-creds", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/cdp-creds", + "createdTime": "2026-05-12T21:18:46.009Z", + "lastUpdatedTime": "2026-05-12T21:33:40.066Z" + }, + { + "name": "paymgr-cdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/paymgr-cdp-cdp", + "createdTime": "2026-05-21T21:19:41.791Z", + "lastUpdatedTime": "2026-05-22T04:40:50.491Z" + }, + { + "name": "pmgr-cdp-cdp", + "credentialProviderVendor": "CoinbaseCDP", + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/pmgr-cdp-cdp", + "createdTime": "2026-05-22T01:59:54.168Z", + "lastUpdatedTime": "2026-05-22T02:42:24.884Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/identity/payment-credential-provider/get/index.tsx b/src/handlers/identity/payment-credential-provider/get/index.tsx new file mode 100644 index 000000000..cc9b19f4e --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/get/index.tsx @@ -0,0 +1,24 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetPaymentCredentialProviderHandler = (core: Core) => + createHandler({ + name: "get", + description: "get a payment credential provider", + flags: [flag("name", "the payment credential provider name (required)", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags.name) { + throw new InputValidationError("required option '--name ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.identity.getPaymentCredentialProvider(flags.name, coreOptsFromCtx(ctx)), + ); + }, + }); diff --git a/src/handlers/identity/payment-credential-provider/index.tsx b/src/handlers/identity/payment-credential-provider/index.tsx new file mode 100644 index 000000000..ed8323500 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/index.tsx @@ -0,0 +1,14 @@ +import { Router } from "../../../router"; +import { renderTui } from "../../../tui"; +import type { AppIO } from "../../../io"; +import type { Core } from "../../types"; +import { createGetPaymentCredentialProviderHandler } from "./get"; +import { createListPaymentCredentialProvidersHandler } from "./list"; + +export function createPaymentCredentialProviderHandler(core: Core, io: AppIO): Router { + return new Router("payment-credential-provider", "manage payment credential providers") + .default(renderTui(core, io)) + .supportedTuiCommands() + .handler(createGetPaymentCredentialProviderHandler(core)) + .handler(createListPaymentCredentialProvidersHandler(core)); +} diff --git a/src/handlers/identity/payment-credential-provider/list/index.tsx b/src/handlers/identity/payment-credential-provider/list/index.tsx new file mode 100644 index 000000000..65bc22918 --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/list/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListPaymentCredentialProvidersHandler = (core: Core) => + createHandler({ + name: "list", + description: "list payment credential providers", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + ctx + .require(JsonRendererKey) + .renderJson( + await core.identity.listPaymentCredentialProviders( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx new file mode 100644 index 000000000..65b7003dc --- /dev/null +++ b/src/handlers/identity/payment-credential-provider/paymentCredentialProvider.read.test.tsx @@ -0,0 +1,125 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; +import { createRootHandler } from "../../index"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); +const BASE = ["identity", "payment-credential-provider"]; +const FIXTURE_PROVIDER_NAME = "agentcore-cli-payment-fixture"; +const MISSING_PROVIDER_NAME = "agentcore-cli-payment-fixture-2"; + +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient, createLogsClient } = + fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + createLogsClient, + logger: createSilentLogger(), + }); +} + +async function run(args: string[]): Promise { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...BASE, ...args, "--region", REGION]); + return io.stdout(); +} + +describe("payment-credential-provider read-only command hierarchy", () => { + test("registers get and list only, with no create, update, or delete commands", () => { + const root = createRootHandler(createFixtureCore(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const identity = root.children().find((child) => child.name() === "identity"); + const payment = identity + ?.children() + .find((child) => child.name() === "payment-credential-provider"); + + expect(payment?.children().map((child) => child.name())).toEqual(["get", "list"]); + }); + + test("prints command help with --json", async () => { + const stdout = await run(["--json"]); + + expect(stdout).toContain("Usage: agentcore identity payment-credential-provider"); + expect(stdout).toContain("Commands:"); + expect(stdout).toContain("get"); + expect(stdout).toContain("list"); + }); +}); + +describe("payment-credential-provider get", () => { + test("prints a recorded provider as JSON", async () => { + const stdout = await run(["get", "--name", FIXTURE_PROVIDER_NAME]); + + matchGolden(FIXTURES, "get.golden.json", stdout); + expect(JSON.parse(stdout)).toMatchObject({ + name: FIXTURE_PROVIDER_NAME, + credentialProviderVendor: "CoinbaseCDP", + }); + }); + + test.each([ + ["omitted", ["get"]], + ["omitted with --json", ["get", "--json"]], + ["empty", ["get", "--name", ""]], + ] as const)("requires a nonempty --name when %s", async (_label, args) => { + await expect(run([...args])).rejects.toThrow("required option '--name ' not specified"); + }); + + test("preserves the recorded service error name and message", async () => { + await expect(run(["get", "--name", MISSING_PROVIDER_NAME])).rejects.toMatchObject({ + name: "ResourceNotFoundException", + message: "PaymentCredentialProvider not found", + }); + }); +}); + +describe("payment-credential-provider list", () => { + test.each([ + ["without flags", ["list"]], + ["with --json", ["list", "--json"]], + ] as const)("prints recorded providers %s", async (_label, args) => { + const stdout = await run([...args]); + + matchGolden(FIXTURES, "list.golden.json", stdout); + expect(JSON.parse(stdout).credentialProviders).toContainEqual( + expect.objectContaining({ name: FIXTURE_PROVIDER_NAME }), + ); + }); + + test("forwards --max-results and --next-token and preserves pagination tokens", async () => { + const firstPage = await run(["list", "--max-results", "1"]); + matchGolden(FIXTURES, "list-page-1.golden.json", firstPage); + const first = JSON.parse(firstPage); + expect(first.credentialProviders).toHaveLength(1); + expect(first.credentialProviders[0].name).toBe("DeployTest-CdpConn-cdp"); + expect(first.nextToken).toBeString(); + expect(first.nextToken.length).toBeGreaterThan(0); + + const secondPage = await run(["list", "--max-results", "1", "--next-token", first.nextToken]); + matchGolden(FIXTURES, "list-page-2.golden.json", secondPage); + const second = JSON.parse(secondPage); + expect(second.credentialProviders).toHaveLength(1); + expect(second.credentialProviders[0].name).toBe("InvokeMgr-InvokeCdp-cdp"); + expect(second.nextToken).toBeString(); + expect(second.nextToken).not.toBe(first.nextToken); + }); +}); diff --git a/src/handlers/identity/types.tsx b/src/handlers/identity/types.tsx index 575ed40a3..6f8363f90 100644 --- a/src/handlers/identity/types.tsx +++ b/src/handlers/identity/types.tsx @@ -17,6 +17,7 @@ import type { CreatePaymentCredentialProviderResponse, DeletePaymentCredentialProviderResponse, GetPaymentCredentialProviderResponse, + ListPaymentCredentialProvidersResponse, UpdatePaymentCredentialProviderRequest, UpdatePaymentCredentialProviderResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; @@ -75,8 +76,9 @@ export interface CoreIdentityClient { ): Promise; // Payment credential providers hold a payment vendor's own credentials (a Coinbase - // CDP API key and wallet secret, or Privy app and authorization secrets). They are - // provisioned by `project deploy` rather than an `agentcore identity` subcommand. + // CDP API key and wallet secret, or Privy app and authorization secrets). They back + // payment connectors and can be inspected with `identity payment-credential-provider`. + // Project deployment uses the write operations below. createPaymentCredentialProvider( input: CreatePaymentCredentialProviderInput, options: CoreOptions, @@ -85,6 +87,11 @@ export interface CoreIdentityClient { name: string, options: CoreOptions, ): Promise; + listPaymentCredentialProviders( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; updatePaymentCredentialProvider( input: UpdatePaymentCredentialProviderInput, options: CoreOptions, diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index d67f11900..f8f60bd8e 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -5,6 +5,7 @@ import { createGatewayHandler } from "./gateway/index.tsx"; import { createHarnessHandler } from "./harness/index.tsx"; import { createIdentityHandler } from "./identity/index.tsx"; import { createMemoryHandler } from "./memory/index.tsx"; +import { createPaymentHandler } from "./payment/index.tsx"; import { createRuntimeHandler } from "./runtime/index.tsx"; import { DebugKey, EndpointKey, JsonKey, RegionKey } from "./keys.tsx"; import { createConfigHandler } from "./config/"; @@ -72,6 +73,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router root.handler(createRuntimeHandler(core, io)); root.handler(createMemoryHandler(core, io)); root.handler(createGatewayHandler(core, io)); + root.handler(createPaymentHandler(core, io)); root.handler(createEvalHandler(core, io)); root.handler(createFeedbackHandler(core, io)); root.handler(createConfigHandler()); diff --git a/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.c9616c307ec6a4ea.json b/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.c9616c307ec6a4ea.json new file mode 100644 index 000000000..c6c5a83b1 --- /dev/null +++ b/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.c9616c307ec6a4ea.json @@ -0,0 +1,17 @@ +{ + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentManagerId": "mypaymentmanageraidandal-gx3nxzaira", + "name": "MyPaymentManagerAidandal", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-PayBash-default-PaymentMyPaymentManagerAi-gyKcmOGL9YyA", + "createdAt": { + "$date": "2026-06-08T18:11:41.129Z" + }, + "lastUpdatedAt": { + "$date": "2026-06-08T18:11:41.129Z" + }, + "status": "READY", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:workload-identity-directory/default/workload-identity/mypaymentmanageraidandal-gx3nxzaira" + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.d806a2d236e5ec0c.json b/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.d806a2d236e5ec0c.json new file mode 100644 index 000000000..ead47a54f --- /dev/null +++ b/src/handlers/payment/__fixtures__/GetPaymentManagerCommand.d806a2d236e5ec0c.json @@ -0,0 +1,17 @@ +{ + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanager-o4ks3qfgtb", + "paymentManagerId": "mypaymentmanager-o4ks3qfgtb", + "name": "MyPaymentManager", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/paytest_PaymentRetrievalRole", + "createdAt": { + "$date": "2026-05-05T03:02:59.201Z" + }, + "lastUpdatedAt": { + "$date": "2026-05-05T03:02:59.201Z" + }, + "status": "READY", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:workload-identity-directory/default/workload-identity/mypaymentmanager-o4ks3qfgtb" + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/ListPaymentManagersCommand.23f97c9dcdd6350b.json b/src/handlers/payment/__fixtures__/ListPaymentManagersCommand.23f97c9dcdd6350b.json new file mode 100644 index 000000000..a3c9eaa3a --- /dev/null +++ b/src/handlers/payment/__fixtures__/ListPaymentManagersCommand.23f97c9dcdd6350b.json @@ -0,0 +1,103 @@ +{ + "paymentManagers": [ + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/brf6410be0-56loajl19z", + "paymentManagerId": "brf6410be0-56loajl19z", + "name": "brf6410be0", + "authorizerType": "CUSTOM_JWT", + "roleArn": "arn:aws:iam::603141041947:role/pr3rolef6410be0", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-06-02T16:59:01.552Z" + }, + "description": "r", + "createdAt": { + "$date": "2026-06-02T16:59:01.552Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/cfn19maymgr-mncapbffse", + "paymentManagerId": "cfn19maymgr-mncapbffse", + "name": "cfn19maymgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/payments-cfn-verify-PaymentMgrRole-MxByV7bxfBVy", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-05-19T18:43:39.589Z" + }, + "createdAt": { + "$date": "2026-05-19T18:43:39.589Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mgr1-mwpbvze4pu", + "paymentManagerId": "mgr1-mwpbvze4pu", + "name": "mgr1", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/Edge2_PaymentRetrievalRole", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-05-05T15:35:08.042Z" + }, + "createdAt": { + "$date": "2026-05-05T15:35:08.042Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanager-o4ks3qfgtb", + "paymentManagerId": "mypaymentmanager-o4ks3qfgtb", + "name": "MyPaymentManager", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/paytest_PaymentRetrievalRole", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-05-05T03:02:59.201Z" + }, + "createdAt": { + "$date": "2026-05-05T03:02:59.201Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentManagerId": "mypaymentmanageraidandal-gx3nxzaira", + "name": "MyPaymentManagerAidandal", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-PayBash-default-PaymentMyPaymentManagerAi-gyKcmOGL9YyA", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-06-08T18:11:41.129Z" + }, + "createdAt": { + "$date": "2026-06-08T18:11:41.129Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/paymgr-tt8ivlyv76", + "paymentManagerId": "paymgr-tt8ivlyv76", + "name": "paymgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-BugBashGW-defau-PaymentpaymgrResourceRetr-wv9Le7KK724Y", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-05-21T21:28:13.357Z" + }, + "createdAt": { + "$date": "2026-05-21T21:28:13.357Z" + } + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/pmgr-amdytlqv7l", + "paymentManagerId": "pmgr-amdytlqv7l", + "name": "pmgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-BugBashFresh-de-PaymentpmgrResourceRetrie-DJKtx7oJy58m", + "status": "READY", + "lastUpdatedAt": { + "$date": "2026-05-22T02:27:29.922Z" + }, + "createdAt": { + "$date": "2026-05-22T02:27:29.922Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/connector/GetPaymentConnectorCommand.9f8dfd59b8af870.json b/src/handlers/payment/__fixtures__/connector/GetPaymentConnectorCommand.9f8dfd59b8af870.json new file mode 100644 index 000000000..6db03fc63 --- /dev/null +++ b/src/handlers/payment/__fixtures__/connector/GetPaymentConnectorCommand.9f8dfd59b8af870.json @@ -0,0 +1,20 @@ +{ + "paymentConnectorId": "agentcorecliconnectore2e-6rodjuiuig", + "name": "AgentCoreCliConnectorE2E", + "type": "CoinbaseCDP", + "credentialProviderConfigurations": [ + { + "coinbaseCDP": { + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp" + } + } + ], + "createdAt": { + "$date": "2026-09-08T20:14:31.151Z" + }, + "lastUpdatedAt": { + "$date": "2026-09-08T20:14:32.139Z" + }, + "status": "READY", + "description": "Updated by the agentcore CLI end-to-end test" +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/connector/ListPaymentConnectorsCommand.c9616c307ec6a4ea.json b/src/handlers/payment/__fixtures__/connector/ListPaymentConnectorsCommand.c9616c307ec6a4ea.json new file mode 100644 index 000000000..f6666ada4 --- /dev/null +++ b/src/handlers/payment/__fixtures__/connector/ListPaymentConnectorsCommand.c9616c307ec6a4ea.json @@ -0,0 +1,16 @@ +{ + "paymentConnectors": [ + { + "paymentConnectorId": "agentcorecliconnectore2e-6rodjuiuig", + "name": "AgentCoreCliConnectorE2E", + "type": "CoinbaseCDP", + "status": "READY" + }, + { + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "name": "MyCdpConnectorAidandal", + "type": "CoinbaseCDP", + "status": "READY" + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/connector/connector-get.golden.json b/src/handlers/payment/__fixtures__/connector/connector-get.golden.json new file mode 100644 index 000000000..bc91b1af6 --- /dev/null +++ b/src/handlers/payment/__fixtures__/connector/connector-get.golden.json @@ -0,0 +1,16 @@ +{ + "paymentConnectorId": "agentcorecliconnectore2e-6rodjuiuig", + "name": "AgentCoreCliConnectorE2E", + "type": "CoinbaseCDP", + "credentialProviderConfigurations": [ + { + "coinbaseCDP": { + "credentialProviderArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:token-vault/default/paymentcredentialprovider/MyPaymentManagerAidandal-MyCdpConnectorAidandal-cdp" + } + } + ], + "createdAt": "2026-09-08T20:14:31.151Z", + "lastUpdatedAt": "2026-09-08T20:14:32.139Z", + "status": "READY", + "description": "Updated by the agentcore CLI end-to-end test" +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/connector/connector-list.golden.json b/src/handlers/payment/__fixtures__/connector/connector-list.golden.json new file mode 100644 index 000000000..f6666ada4 --- /dev/null +++ b/src/handlers/payment/__fixtures__/connector/connector-list.golden.json @@ -0,0 +1,16 @@ +{ + "paymentConnectors": [ + { + "paymentConnectorId": "agentcorecliconnectore2e-6rodjuiuig", + "name": "AgentCoreCliConnectorE2E", + "type": "CoinbaseCDP", + "status": "READY" + }, + { + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "name": "MyCdpConnectorAidandal", + "type": "CoinbaseCDP", + "status": "READY" + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/instrument/GetPaymentInstrumentCommand.1f6c9a8ae4328e61.json b/src/handlers/payment/__fixtures__/instrument/GetPaymentInstrumentCommand.1f6c9a8ae4328e61.json new file mode 100644 index 000000000..045ee3a27 --- /dev/null +++ b/src/handlers/payment/__fixtures__/instrument/GetPaymentInstrumentCommand.1f6c9a8ae4328e61.json @@ -0,0 +1,23 @@ +{ + "paymentInstrument": { + "paymentInstrumentId": "payment-instrument-CG2Tl7U1HnCGfHW", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "userId": "agentcore-cli-e2e", + "paymentInstrumentType": "EMBEDDED_CRYPTO_WALLET", + "paymentInstrumentDetails": { + "embeddedCryptoWallet": { + "network": "ETHEREUM", + "walletAddress": "0x93581aB831Cc862aA451E91fBf8365e098930859", + "redirectUrl": "https://hub.cdp.coinbase.com/e3eae6406a52" + } + }, + "createdAt": { + "$date": "2026-09-08T20:24:40.849Z" + }, + "status": "ACTIVE", + "updatedAt": { + "$date": "2026-09-08T20:24:41.673Z" + } + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/instrument/ListPaymentInstrumentsCommand.df0e58fb9a8e3483.json b/src/handlers/payment/__fixtures__/instrument/ListPaymentInstrumentsCommand.df0e58fb9a8e3483.json new file mode 100644 index 000000000..d7db0fd2a --- /dev/null +++ b/src/handlers/payment/__fixtures__/instrument/ListPaymentInstrumentsCommand.df0e58fb9a8e3483.json @@ -0,0 +1,18 @@ +{ + "paymentInstruments": [ + { + "paymentInstrumentId": "payment-instrument-CG2Tl7U1HnCGfHW", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "userId": "agentcore-cli-e2e", + "paymentInstrumentType": "EMBEDDED_CRYPTO_WALLET", + "status": "ACTIVE", + "createdAt": { + "$date": "2026-09-08T20:24:40.849Z" + }, + "updatedAt": { + "$date": "2026-09-08T20:24:41.673Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/instrument/instrument-get.golden.json b/src/handlers/payment/__fixtures__/instrument/instrument-get.golden.json new file mode 100644 index 000000000..468d5d808 --- /dev/null +++ b/src/handlers/payment/__fixtures__/instrument/instrument-get.golden.json @@ -0,0 +1,19 @@ +{ + "paymentInstrument": { + "paymentInstrumentId": "payment-instrument-CG2Tl7U1HnCGfHW", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "userId": "agentcore-cli-e2e", + "paymentInstrumentType": "EMBEDDED_CRYPTO_WALLET", + "paymentInstrumentDetails": { + "embeddedCryptoWallet": { + "network": "ETHEREUM", + "walletAddress": "0x93581aB831Cc862aA451E91fBf8365e098930859", + "redirectUrl": "https://hub.cdp.coinbase.com/e3eae6406a52" + } + }, + "createdAt": "2026-09-08T20:24:40.849Z", + "status": "ACTIVE", + "updatedAt": "2026-09-08T20:24:41.673Z" + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/instrument/instrument-list.golden.json b/src/handlers/payment/__fixtures__/instrument/instrument-list.golden.json new file mode 100644 index 000000000..52811605b --- /dev/null +++ b/src/handlers/payment/__fixtures__/instrument/instrument-list.golden.json @@ -0,0 +1,14 @@ +{ + "paymentInstruments": [ + { + "paymentInstrumentId": "payment-instrument-CG2Tl7U1HnCGfHW", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentConnectorId": "mycdpconnectoraidandal-okve8guw4y", + "userId": "agentcore-cli-e2e", + "paymentInstrumentType": "EMBEDDED_CRYPTO_WALLET", + "status": "ACTIVE", + "createdAt": "2026-09-08T20:24:40.849Z", + "updatedAt": "2026-09-08T20:24:41.673Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/manager-get.golden.json b/src/handlers/payment/__fixtures__/manager-get.golden.json new file mode 100644 index 000000000..3821d1548 --- /dev/null +++ b/src/handlers/payment/__fixtures__/manager-get.golden.json @@ -0,0 +1,13 @@ +{ + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanager-o4ks3qfgtb", + "paymentManagerId": "mypaymentmanager-o4ks3qfgtb", + "name": "MyPaymentManager", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/paytest_PaymentRetrievalRole", + "createdAt": "2026-05-05T03:02:59.201Z", + "lastUpdatedAt": "2026-05-05T03:02:59.201Z", + "status": "READY", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:workload-identity-directory/default/workload-identity/mypaymentmanager-o4ks3qfgtb" + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/manager-list.golden.json b/src/handlers/payment/__fixtures__/manager-list.golden.json new file mode 100644 index 000000000..5a0f00835 --- /dev/null +++ b/src/handlers/payment/__fixtures__/manager-list.golden.json @@ -0,0 +1,75 @@ +{ + "paymentManagers": [ + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/brf6410be0-56loajl19z", + "paymentManagerId": "brf6410be0-56loajl19z", + "name": "brf6410be0", + "authorizerType": "CUSTOM_JWT", + "roleArn": "arn:aws:iam::603141041947:role/pr3rolef6410be0", + "status": "READY", + "lastUpdatedAt": "2026-06-02T16:59:01.552Z", + "description": "r", + "createdAt": "2026-06-02T16:59:01.552Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/cfn19maymgr-mncapbffse", + "paymentManagerId": "cfn19maymgr-mncapbffse", + "name": "cfn19maymgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/payments-cfn-verify-PaymentMgrRole-MxByV7bxfBVy", + "status": "READY", + "lastUpdatedAt": "2026-05-19T18:43:39.589Z", + "createdAt": "2026-05-19T18:43:39.589Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mgr1-mwpbvze4pu", + "paymentManagerId": "mgr1-mwpbvze4pu", + "name": "mgr1", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/Edge2_PaymentRetrievalRole", + "status": "READY", + "lastUpdatedAt": "2026-05-05T15:35:08.042Z", + "createdAt": "2026-05-05T15:35:08.042Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanager-o4ks3qfgtb", + "paymentManagerId": "mypaymentmanager-o4ks3qfgtb", + "name": "MyPaymentManager", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/paytest_PaymentRetrievalRole", + "status": "READY", + "lastUpdatedAt": "2026-05-05T03:02:59.201Z", + "createdAt": "2026-05-05T03:02:59.201Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "paymentManagerId": "mypaymentmanageraidandal-gx3nxzaira", + "name": "MyPaymentManagerAidandal", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-PayBash-default-PaymentMyPaymentManagerAi-gyKcmOGL9YyA", + "status": "READY", + "lastUpdatedAt": "2026-06-08T18:11:41.129Z", + "createdAt": "2026-06-08T18:11:41.129Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/paymgr-tt8ivlyv76", + "paymentManagerId": "paymgr-tt8ivlyv76", + "name": "paymgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-BugBashGW-defau-PaymentpaymgrResourceRetr-wv9Le7KK724Y", + "status": "READY", + "lastUpdatedAt": "2026-05-21T21:28:13.357Z", + "createdAt": "2026-05-21T21:28:13.357Z" + }, + { + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/pmgr-amdytlqv7l", + "paymentManagerId": "pmgr-amdytlqv7l", + "name": "pmgr", + "authorizerType": "AWS_IAM", + "roleArn": "arn:aws:iam::603141041947:role/AgentCore-BugBashFresh-de-PaymentpmgrResourceRetrie-DJKtx7oJy58m", + "status": "READY", + "lastUpdatedAt": "2026-05-22T02:27:29.922Z", + "createdAt": "2026-05-22T02:27:29.922Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/session/GetPaymentSessionCommand.86f3e58b4b886322.json b/src/handlers/payment/__fixtures__/session/GetPaymentSessionCommand.86f3e58b4b886322.json new file mode 100644 index 000000000..9bc3a7437 --- /dev/null +++ b/src/handlers/payment/__fixtures__/session/GetPaymentSessionCommand.86f3e58b4b886322.json @@ -0,0 +1,29 @@ +{ + "paymentSession": { + "paymentSessionId": "payment-session-nq812U4e1BJIfw1", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "userId": "agentcore-cli-e2e", + "expiryTimeInMinutes": 15, + "createdAt": { + "$date": "2026-09-08T20:20:28.618Z" + }, + "updatedAt": { + "$date": "2026-09-08T20:20:28.618Z" + }, + "limits": { + "maxSpendAmount": { + "value": "1", + "currency": "USD" + } + }, + "availableLimits": { + "availableSpendAmount": { + "value": "1", + "currency": "USD" + }, + "updatedAt": { + "$date": "2026-09-08T20:20:28.697Z" + } + } + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/session/ListPaymentSessionsCommand.a9c4632c14ae0699.json b/src/handlers/payment/__fixtures__/session/ListPaymentSessionsCommand.a9c4632c14ae0699.json new file mode 100644 index 000000000..d73d725bc --- /dev/null +++ b/src/handlers/payment/__fixtures__/session/ListPaymentSessionsCommand.a9c4632c14ae0699.json @@ -0,0 +1,16 @@ +{ + "paymentSessions": [ + { + "paymentSessionId": "payment-session-nq812U4e1BJIfw1", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "userId": "agentcore-cli-e2e", + "expiryTimeInMinutes": 15, + "createdAt": { + "$date": "2026-09-08T20:20:28.618Z" + }, + "updatedAt": { + "$date": "2026-09-08T20:20:28.618Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/session/session-get.golden.json b/src/handlers/payment/__fixtures__/session/session-get.golden.json new file mode 100644 index 000000000..cd7ec06a6 --- /dev/null +++ b/src/handlers/payment/__fixtures__/session/session-get.golden.json @@ -0,0 +1,23 @@ +{ + "paymentSession": { + "paymentSessionId": "payment-session-nq812U4e1BJIfw1", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "userId": "agentcore-cli-e2e", + "expiryTimeInMinutes": 15, + "createdAt": "2026-09-08T20:20:28.618Z", + "updatedAt": "2026-09-08T20:20:28.618Z", + "limits": { + "maxSpendAmount": { + "value": "1", + "currency": "USD" + } + }, + "availableLimits": { + "availableSpendAmount": { + "value": "1", + "currency": "USD" + }, + "updatedAt": "2026-09-08T20:20:28.697Z" + } + } +} \ No newline at end of file diff --git a/src/handlers/payment/__fixtures__/session/session-list.golden.json b/src/handlers/payment/__fixtures__/session/session-list.golden.json new file mode 100644 index 000000000..8c6e3c6ca --- /dev/null +++ b/src/handlers/payment/__fixtures__/session/session-list.golden.json @@ -0,0 +1,12 @@ +{ + "paymentSessions": [ + { + "paymentSessionId": "payment-session-nq812U4e1BJIfw1", + "paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/mypaymentmanageraidandal-gx3nxzaira", + "userId": "agentcore-cli-e2e", + "expiryTimeInMinutes": 15, + "createdAt": "2026-09-08T20:20:28.618Z", + "updatedAt": "2026-09-08T20:20:28.618Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/payment/connector/get/index.tsx b/src/handlers/payment/connector/get/index.tsx new file mode 100644 index 000000000..16ef12d80 --- /dev/null +++ b/src/handlers/payment/connector/get/index.tsx @@ -0,0 +1,47 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import type { AppIO } from "../../../../io"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import { JsonKey } from "../../../keys"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetPaymentConnectorHandler = (core: Core, io: AppIO) => + createHandler({ + name: "get", + description: "get a payment connector by id", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag("connector-id", "the payment connector ID (required)", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["connector-id"]) { + throw new InputValidationError( + "required option '--connector-id ' not specified", + ); + } + + const response = await core.payment.getPaymentConnector( + flags["manager-id"], + flags["connector-id"], + coreOptsFromCtx(ctx), + ); + ctx.require(JsonRendererKey).renderJson(response); + + // A lapsed or failed OAuth consent is terminal for a Quick Create + // connector: the service issues no second authorization URL. + if ( + !ctx.require(JsonKey) && + (response.status === "AUTHENTICATION_EXPIRED" || + response.status === "AUTHENTICATION_FAILED") + ) { + io.stderr.write( + `warning: connector status is ${response.status}; its authorization URL cannot be renewed.\n`, + ); + } + }, + }); diff --git a/src/handlers/payment/connector/index.tsx b/src/handlers/payment/connector/index.tsx new file mode 100644 index 000000000..b66523054 --- /dev/null +++ b/src/handlers/payment/connector/index.tsx @@ -0,0 +1,13 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { renderTui } from "../../../tui"; +import type { Core } from "../../types"; +import { createGetPaymentConnectorHandler } from "./get"; +import { createListPaymentConnectorsHandler } from "./list"; + +export function createPaymentConnectorHandler(core: Core, io: AppIO): Router { + return new Router("connector", "manage connectors under a payment manager") + .default(renderTui(core, io)) + .handler(createGetPaymentConnectorHandler(core, io)) + .handler(createListPaymentConnectorsHandler(core)); +} diff --git a/src/handlers/payment/connector/list/index.tsx b/src/handlers/payment/connector/list/index.tsx new file mode 100644 index 000000000..086ec4102 --- /dev/null +++ b/src/handlers/payment/connector/list/index.tsx @@ -0,0 +1,33 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListPaymentConnectorsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list the connectors of a payment manager", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.payment.listPaymentConnectors( + flags["manager-id"], + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/payment/index.tsx b/src/handlers/payment/index.tsx new file mode 100644 index 000000000..c2ebb5f37 --- /dev/null +++ b/src/handlers/payment/index.tsx @@ -0,0 +1,24 @@ +import { withTuiOnEmptyFlagsAndArgs } from "../../middleware"; +import { Router } from "../../router"; +import { renderTui } from "../../tui"; +import type { AppIO } from "../../io"; +import type { Core } from "../types"; +import { createPaymentConnectorHandler } from "./connector"; +import { createPaymentInstrumentHandler } from "./instrument"; +import { createPaymentManagerHandler } from "./manager"; +import { createPaymentSessionHandler } from "./session"; + +export function createPaymentHandler(core: Core, io: AppIO): Router { + return ( + new Router("payment", "manage AgentCore Payments") + .use(withTuiOnEmptyFlagsAndArgs(core, io)) + .default(renderTui(core, io)) + // No payment screens ship yet: every leaf runs from the command line and the + // menus list them as such. Widen this as screens land. + .supportedTuiCommands() + .handler(createPaymentManagerHandler(core, io)) + .handler(createPaymentConnectorHandler(core, io)) + .handler(createPaymentSessionHandler(core, io)) + .handler(createPaymentInstrumentHandler(core, io)) + ); +} diff --git a/src/handlers/payment/instrument/balance/balance.test.tsx b/src/handlers/payment/instrument/balance/balance.test.tsx new file mode 100644 index 000000000..6935c88d4 --- /dev/null +++ b/src/handlers/payment/instrument/balance/balance.test.tsx @@ -0,0 +1,123 @@ +import { expect, mock, test } from "bun:test"; +import { + GetPaymentInstrumentBalanceCommand, + GetPaymentInstrumentCommand, + type BedrockAgentCoreClient, +} from "@aws-sdk/client-bedrock-agentcore"; +import type { BedrockAgentCoreControlClient } from "@aws-sdk/client-bedrock-agentcore-control"; +import { CoreClient } from "../../../../core"; +import { createRootHandler } from "../../../index"; +import { createSilentLogger, TestGlobalConfigAccessor, testIO } from "../../../../testing"; + +const ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/manager"; +const scope = ["--manager-id", "manager", "--user-id", "user", "--instrument-id", "instrument"]; +// Synthetic response: fixture replay does not require a funded wallet. +const balance = { + paymentInstrumentId: "instrument", + tokenBalance: { + amount: "9007199254740993123456789", + decimals: 6, + token: "USDC", + network: "ETHEREUM", + chain: "BASE_SEPOLIA", + }, +}; + +function setup(response: object = balance) { + const send = mock(async (_command: unknown) => response); + const unexpectedClient = () => { + throw new Error("Unexpected AWS client"); + }; + const core = new CoreClient({ + createControlClient: () => + ({ + send: async () => ({ paymentManagerArn: ARN, authorizerType: "AWS_IAM" }), + }) as unknown as BedrockAgentCoreControlClient, + createDataClient: () => ({ send }) as unknown as BedrockAgentCoreClient, + createIamClient: unexpectedClient, + createLogsClient: unexpectedClient, + logger: createSilentLogger(), + }); + const io = testIO(); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + return { + send, + io, + run: (args: string[]) => + root.route([ + "node", + "agentcore", + "payment", + "instrument", + ...args, + "--region", + "us-west-2", + "--json", + ]), + }; +} + +test("balance sends the selected chain and complete scope, defaults USDC, and preserves atomic precision", async () => { + const { run, send, io } = setup(); + await run([ + "balance", + ...scope, + "--connector-id", + "connector", + "--chain", + "BASE_SEPOLIA", + "--agent-name", + "agent", + ]); + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0]?.[0]).toBeInstanceOf(GetPaymentInstrumentBalanceCommand); + expect((send.mock.calls[0]![0] as GetPaymentInstrumentBalanceCommand).input).toEqual({ + paymentManagerArn: ARN, + userId: "user", + paymentInstrumentId: "instrument", + paymentConnectorId: "connector", + chain: "BASE_SEPOLIA", + token: "USDC", + agentName: "agent", + }); + expect(JSON.parse(io.stdout())).toEqual(balance); +}); + +test("preserves a successful zero balance but never turns an error into zero", async () => { + const zero = { ...balance, tokenBalance: { ...balance.tokenBalance, amount: "0" } }; + const success = setup(zero); + const args = ["balance", ...scope, "--connector-id", "connector", "--chain", "BASE"]; + await success.run(args); + expect(JSON.parse(success.io.stdout())).toEqual(zero); + + const failure = setup(); + const error = new Error("No USDC balance is available"); + failure.send.mockRejectedValueOnce(error); + await expect(failure.run(args)).rejects.toBe(error); + expect(failure.io.stdout()).toBe(""); +}); + +test.each([ + [[], "--chain"], + [["--chain", "base"], "Invalid value for option '--chain'"], + [["--chain", "BASE", "--token", "ETH"], "Invalid value for option '--token'"], +] as const)("requires an explicit supported chain and token: %j", async (flags, message) => { + const { run, send } = setup(); + await expect(run(["balance", ...scope, "--connector-id", "connector", ...flags])).rejects.toThrow( + message, + ); + expect(send).not.toHaveBeenCalled(); +}); + +test("instrument get remains independent of balance and requires no chain", async () => { + const response = { paymentInstrument: { paymentInstrumentId: "instrument", status: "ACTIVE" } }; + const { run, send, io } = setup(response); + await run(["get", ...scope]); + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0]?.[0]).toBeInstanceOf(GetPaymentInstrumentCommand); + expect(JSON.parse(io.stdout())).toEqual(response); +}); diff --git a/src/handlers/payment/instrument/balance/index.tsx b/src/handlers/payment/instrument/balance/index.tsx new file mode 100644 index 000000000..444089e8b --- /dev/null +++ b/src/handlers/payment/instrument/balance/index.tsx @@ -0,0 +1,78 @@ +import { BlockchainChainId, InstrumentBalanceToken } from "@aws-sdk/client-bedrock-agentcore"; +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import type { GetPaymentInstrumentBalanceInput } from "../../types"; + +export const createGetPaymentInstrumentBalanceHandler = (core: Core) => + createHandler({ + name: "balance", + description: "get a payment instrument's token balance on a specific chain", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag( + "connector-id", + "the instrument's payment connector ID (required)", + z.string().optional(), + ), + flag("instrument-id", "the payment instrument ID (required)", z.string().optional()), + flag( + "user-id", + "the application user ID associated with the instrument (required)", + z.string().optional(), + ), + flag( + "chain", + `the blockchain chain to query (required; ${Object.values(BlockchainChainId).join(" | ")})`, + z.enum(BlockchainChainId).optional(), + ), + flag( + "token", + `the token to query (${Object.values(InstrumentBalanceToken).join(" | ")})`, + z.enum(InstrumentBalanceToken).default(InstrumentBalanceToken.USDC), + ), + flag( + "agent-name", + "optional observability label, not an agent selector", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["user-id"]) { + throw new InputValidationError("required option '--user-id ' not specified"); + } + if (!flags["connector-id"]) { + throw new InputValidationError( + "required option '--connector-id ' not specified", + ); + } + if (!flags["instrument-id"]) { + throw new InputValidationError( + "required option '--instrument-id ' not specified", + ); + } + if (!flags["chain"]) { + throw new InputValidationError("required option '--chain ' not specified"); + } + + const request: GetPaymentInstrumentBalanceInput = { + managerId: flags["manager-id"], + userId: flags["user-id"], + paymentConnectorId: flags["connector-id"], + paymentInstrumentId: flags["instrument-id"], + chain: flags["chain"], + token: flags["token"], + ...(flags["agent-name"] ? { agentName: flags["agent-name"] } : {}), + }; + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.getPaymentInstrumentBalance(request, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/instrument/get/index.tsx b/src/handlers/payment/instrument/get/index.tsx new file mode 100644 index 000000000..f03396aeb --- /dev/null +++ b/src/handlers/payment/instrument/get/index.tsx @@ -0,0 +1,57 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import type { GetPaymentInstrumentInput } from "../../types"; + +export const createGetPaymentInstrumentHandler = (core: Core) => + createHandler({ + name: "get", + description: "get a payment instrument by id", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag("instrument-id", "the payment instrument ID (required)", z.string().optional()), + flag( + "user-id", + "the application user ID associated with the instrument (required)", + z.string().optional(), + ), + flag( + "connector-id", + "optionally restrict the lookup to this payment connector", + z.string().optional(), + ), + flag( + "agent-name", + "optional observability label, not an agent selector", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["user-id"]) { + throw new InputValidationError("required option '--user-id ' not specified"); + } + if (!flags["instrument-id"]) { + throw new InputValidationError( + "required option '--instrument-id ' not specified", + ); + } + + const request: GetPaymentInstrumentInput = { + managerId: flags["manager-id"], + userId: flags["user-id"], + paymentInstrumentId: flags["instrument-id"], + ...(flags["agent-name"] ? { agentName: flags["agent-name"] } : {}), + ...(flags["connector-id"] ? { paymentConnectorId: flags["connector-id"] } : {}), + }; + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.getPaymentInstrument(request, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/instrument/index.tsx b/src/handlers/payment/instrument/index.tsx new file mode 100644 index 000000000..6b76db261 --- /dev/null +++ b/src/handlers/payment/instrument/index.tsx @@ -0,0 +1,15 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { renderTui } from "../../../tui"; +import type { Core } from "../../types"; +import { createGetPaymentInstrumentHandler } from "./get"; +import { createListPaymentInstrumentsHandler } from "./list"; +import { createGetPaymentInstrumentBalanceHandler } from "./balance"; + +export function createPaymentInstrumentHandler(core: Core, io: AppIO): Router { + return new Router("instrument", "manage payment instruments (embedded crypto wallets)") + .default(renderTui(core, io)) + .handler(createGetPaymentInstrumentHandler(core)) + .handler(createListPaymentInstrumentsHandler(core)) + .handler(createGetPaymentInstrumentBalanceHandler(core)); +} diff --git a/src/handlers/payment/instrument/list/index.tsx b/src/handlers/payment/instrument/list/index.tsx new file mode 100644 index 000000000..5df04de50 --- /dev/null +++ b/src/handlers/payment/instrument/list/index.tsx @@ -0,0 +1,54 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import type { ListPaymentInstrumentsInput } from "../../types"; + +export const createListPaymentInstrumentsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list an application user's payment instruments under a payment manager", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag( + "user-id", + "the application user ID to list instruments for (required)", + z.string().optional(), + ), + flag( + "connector-id", + "optionally filter instruments by this payment connector", + z.string().optional(), + ), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + flag( + "agent-name", + "optional observability label, not an agent selector", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["user-id"]) { + throw new InputValidationError("required option '--user-id ' not specified"); + } + + const request: ListPaymentInstrumentsInput = { + managerId: flags["manager-id"], + userId: flags["user-id"], + ...(flags["agent-name"] ? { agentName: flags["agent-name"] } : {}), + ...(flags["connector-id"] ? { paymentConnectorId: flags["connector-id"] } : {}), + ...(flags["next-token"] ? { nextToken: flags["next-token"] } : {}), + ...(flags["max-results"] !== undefined ? { maxResults: flags["max-results"] } : {}), + }; + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.listPaymentInstruments(request, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/manager/get/index.tsx b/src/handlers/payment/manager/get/index.tsx new file mode 100644 index 000000000..ad9b63f73 --- /dev/null +++ b/src/handlers/payment/manager/get/index.tsx @@ -0,0 +1,22 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetPaymentManagerHandler = (core: Core) => + createHandler({ + name: "get", + description: "get a payment manager by id", + flags: [flag("id", "the payment manager ID (required)", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags.id) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.getPaymentManager(flags.id, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/manager/index.tsx b/src/handlers/payment/manager/index.tsx new file mode 100644 index 000000000..1cd6b8652 --- /dev/null +++ b/src/handlers/payment/manager/index.tsx @@ -0,0 +1,13 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { renderTui } from "../../../tui"; +import type { Core } from "../../types"; +import { createGetPaymentManagerHandler } from "./get"; +import { createListPaymentManagersHandler } from "./list"; + +export function createPaymentManagerHandler(core: Core, io: AppIO): Router { + return new Router("manager", "manage AgentCore payment managers") + .default(renderTui(core, io)) + .handler(createGetPaymentManagerHandler(core)) + .handler(createListPaymentManagersHandler(core)); +} diff --git a/src/handlers/payment/manager/list/index.tsx b/src/handlers/payment/manager/list/index.tsx new file mode 100644 index 000000000..27505f616 --- /dev/null +++ b/src/handlers/payment/manager/list/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListPaymentManagersHandler = (core: Core) => + createHandler({ + name: "list", + description: "list payment managers", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + ctx + .require(JsonRendererKey) + .renderJson( + await core.payment.listPaymentManagers( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/payment/payment.read.test.tsx b/src/handlers/payment/payment.read.test.tsx new file mode 100644 index 000000000..2cb8dd8d3 --- /dev/null +++ b/src/handlers/payment/payment.read.test.tsx @@ -0,0 +1,199 @@ +import { expect, mock, spyOn, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../core"; +import { compile, isTuiCommandSupported, ValueContext } from "../../router"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; + +const FIXTURES = join(import.meta.dir, "__fixtures__"); +const MANAGER_ID = "mypaymentmanageraidandal-gx3nxzaira"; +const CONNECTOR_ID = "agentcorecliconnectore2e-6rodjuiuig"; +const INSTRUMENT_CONNECTOR_ID = "mycdpconnectoraidandal-okve8guw4y"; +const SESSION_ID = "payment-session-nq812U4e1BJIfw1"; +const INSTRUMENT_ID = "payment-instrument-CG2Tl7U1HnCGfHW"; +const scope = ["--manager-id", MANAGER_ID, "--user-id", "agentcore-cli-e2e"]; + +function setup(resource = "manager", overrides: Partial> = {}) { + const core = new CoreClient({ + ...fixtureFactories(resource === "connector" ? join(FIXTURES, resource) : FIXTURES), + createDataClient: fixtureFactories(join(FIXTURES, resource)).createDataClient, + ...overrides, + logger: createSilentLogger(), + }); + const io = testIO(); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + return { + core, + io, + root, + run: (args: string[]) => + root.route(["node", "agentcore", "payment", ...args, "--region", "us-west-2"]), + }; +} + +test("registers the read-only command tree without TUI or mutation leaves", () => { + const payment = compile(setup().root, ValueContext.EmptyContext()).commands.find( + (c) => c.name() === "payment", + )!; + expect( + Object.fromEntries( + payment.commands.map((resource) => [resource.name(), resource.commands.map((c) => c.name())]), + ), + ).toEqual({ + manager: ["get", "list"], + connector: ["get", "list"], + session: ["get", "list"], + instrument: ["get", "list", "balance"], + }); + for (const resource of payment.commands) { + for (const command of resource.commands) expect(isTuiCommandSupported(command)).toBe(false); + } +}); + +test.each([ + ["manager", "get", ["--id", "mypaymentmanager-o4ks3qfgtb"]], + ["manager", "list", []], + ["connector", "get", ["--manager-id", MANAGER_ID, "--connector-id", CONNECTOR_ID]], + ["connector", "list", ["--manager-id", MANAGER_ID]], + ["session", "get", [...scope, "--session-id", SESSION_ID]], + ["session", "list", scope], + ["instrument", "get", [...scope, "--instrument-id", INSTRUMENT_ID]], + ["instrument", "list", [...scope, "--connector-id", INSTRUMENT_CONNECTOR_ID]], +] as const)("%s %s renders the recorded response", async (resource, verb, flags) => { + const { run, io } = setup(resource); + await run([resource, verb, ...flags]); + matchGolden( + resource === "manager" ? FIXTURES : join(FIXTURES, resource), + `${resource}-${verb}.golden.json`, + io.stdout(), + ); +}); + +test.each([ + ["manager", [], {}], + ["connector", ["--manager-id", MANAGER_ID], { paymentManagerId: MANAGER_ID }], + ["session", scope, { userId: "agentcore-cli-e2e" }], + [ + "instrument", + [...scope, "--connector-id", INSTRUMENT_CONNECTOR_ID], + { userId: "agentcore-cli-e2e", paymentConnectorId: INSTRUMENT_CONNECTOR_ID }, + ], +] as const)( + "%s list forwards pagination without consuming further pages", + async (resource, flags, expected) => { + const response = { nextToken: "page-3" }; + const send = mock(async (_command: { input: unknown }) => response); + const isData = resource === "session" || resource === "instrument"; + const { run, io } = setup(resource, { + [isData ? "createDataClient" : "createControlClient"]: () => ({ send }) as never, + }); + await run([ + resource, + "list", + ...flags, + "--next-token", + "page+2/=", + "--max-results", + "1", + "--json", + ]); + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0]![0].input).toEqual({ + ...expected, + ...(isData + ? { + paymentManagerArn: `arn:aws:bedrock-agentcore:us-west-2:603141041947:payment-manager/${MANAGER_ID}`, + } + : {}), + nextToken: "page+2/=", + maxResults: 1, + }); + expect(JSON.parse(io.stdout())).toEqual(response); + }, +); + +test("session and instrument reads forward user, agent, and optional connector scope", async () => { + const { core, run } = setup(); + const session = spyOn(core.payment, "getPaymentSession").mockResolvedValue({ + paymentSession: undefined, + }); + const instrument = spyOn(core.payment, "getPaymentInstrument").mockResolvedValue({ + paymentInstrument: undefined, + }); + try { + await run(["session", "get", ...scope, "--session-id", SESSION_ID, "--agent-name", "agent"]); + expect(session).toHaveBeenCalledWith( + { + managerId: MANAGER_ID, + userId: "agentcore-cli-e2e", + paymentSessionId: SESSION_ID, + agentName: "agent", + }, + expect.objectContaining({ region: "us-west-2" }), + ); + await run([ + "instrument", + "get", + ...scope, + "--instrument-id", + INSTRUMENT_ID, + "--connector-id", + INSTRUMENT_CONNECTOR_ID, + ]); + expect(instrument).toHaveBeenCalledWith( + { + managerId: MANAGER_ID, + userId: "agentcore-cli-e2e", + paymentInstrumentId: INSTRUMENT_ID, + paymentConnectorId: INSTRUMENT_CONNECTOR_ID, + }, + expect.objectContaining({ region: "us-west-2" }), + ); + } finally { + session.mockRestore(); + instrument.mockRestore(); + } +}); + +test.each([ + [["manager", "get"], "--id"], + [["connector", "get", "--manager-id", MANAGER_ID], "--connector-id"], + [["session", "get", ...scope], "--session-id"], + [["instrument", "get", ...scope], "--instrument-id"], + [["session", "list", "--manager-id", MANAGER_ID], "--user-id"], + [ + ["instrument", "get", ...scope, "--instrument-id", INSTRUMENT_ID, "--manager-arn", "arn:old"], + "unknown option", + ], +] as const)("rejects incomplete or obsolete selectors: %j", async (args, message) => { + await expect(setup().run([...args])).rejects.toThrow(message); +}); + +test.each(["AUTHENTICATION_EXPIRED", "AUTHENTICATION_FAILED"] as const)( + "connector get reports %s on stderr, except with --json", + async (status) => { + const { core, run, io } = setup("connector"); + const call = spyOn(core.payment, "getPaymentConnector").mockResolvedValue({ status } as never); + try { + const args = ["connector", "get", "--manager-id", MANAGER_ID, "--connector-id", CONNECTOR_ID]; + await run(args); + expect(io.stderr()).toContain("cannot be renewed"); + expect(JSON.parse(io.stdout())).toEqual({ status }); + const stderr = io.stderr(); + await run([...args, "--json"]); + expect(io.stderr()).toBe(stderr); + } finally { + call.mockRestore(); + } + }, +); diff --git a/src/handlers/payment/session/get/index.tsx b/src/handlers/payment/session/get/index.tsx new file mode 100644 index 000000000..09f864be8 --- /dev/null +++ b/src/handlers/payment/session/get/index.tsx @@ -0,0 +1,49 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import type { GetPaymentSessionInput } from "../../types"; + +export const createGetPaymentSessionHandler = (core: Core) => + createHandler({ + name: "get", + description: "get a payment session by id", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag("session-id", "the payment session ID (required)", z.string().optional()), + flag( + "user-id", + "the application user ID associated with the session (required)", + z.string().optional(), + ), + flag( + "agent-name", + "optional observability label, not an agent selector", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["user-id"]) { + throw new InputValidationError("required option '--user-id ' not specified"); + } + if (!flags["session-id"]) { + throw new InputValidationError("required option '--session-id ' not specified"); + } + + const request: GetPaymentSessionInput = { + managerId: flags["manager-id"], + userId: flags["user-id"], + paymentSessionId: flags["session-id"], + ...(flags["agent-name"] ? { agentName: flags["agent-name"] } : {}), + }; + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.getPaymentSession(request, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/session/index.tsx b/src/handlers/payment/session/index.tsx new file mode 100644 index 000000000..dff3e769a --- /dev/null +++ b/src/handlers/payment/session/index.tsx @@ -0,0 +1,13 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { renderTui } from "../../../tui"; +import type { Core } from "../../types"; +import { createGetPaymentSessionHandler } from "./get"; +import { createListPaymentSessionsHandler } from "./list"; + +export function createPaymentSessionHandler(core: Core, io: AppIO): Router { + return new Router("session", "manage payment sessions (budget-limited payment contexts)") + .default(renderTui(core, io)) + .handler(createGetPaymentSessionHandler(core)) + .handler(createListPaymentSessionsHandler(core)); +} diff --git a/src/handlers/payment/session/list/index.tsx b/src/handlers/payment/session/list/index.tsx new file mode 100644 index 000000000..4c97a1bc8 --- /dev/null +++ b/src/handlers/payment/session/list/index.tsx @@ -0,0 +1,48 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; +import type { ListPaymentSessionsInput } from "../../types"; + +export const createListPaymentSessionsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list an application user's payment sessions under a payment manager", + flags: [ + flag("manager-id", "the parent payment manager ID (required)", z.string().optional()), + flag( + "user-id", + "the application user ID to list sessions for (required)", + z.string().optional(), + ), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + flag( + "agent-name", + "optional observability label, not an agent selector", + z.string().optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags["manager-id"]) { + throw new InputValidationError("required option '--manager-id ' not specified"); + } + if (!flags["user-id"]) { + throw new InputValidationError("required option '--user-id ' not specified"); + } + + const request: ListPaymentSessionsInput = { + managerId: flags["manager-id"], + userId: flags["user-id"], + ...(flags["agent-name"] ? { agentName: flags["agent-name"] } : {}), + ...(flags["next-token"] ? { nextToken: flags["next-token"] } : {}), + ...(flags["max-results"] !== undefined ? { maxResults: flags["max-results"] } : {}), + }; + + ctx + .require(JsonRendererKey) + .renderJson(await core.payment.listPaymentSessions(request, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/payment/types.tsx b/src/handlers/payment/types.tsx new file mode 100644 index 000000000..7456a5389 --- /dev/null +++ b/src/handlers/payment/types.tsx @@ -0,0 +1,70 @@ +import type { + GetPaymentConnectorResponse, + GetPaymentManagerResponse, + ListPaymentConnectorsResponse, + ListPaymentManagersResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { + GetPaymentInstrumentRequest, + GetPaymentInstrumentResponse, + GetPaymentInstrumentBalanceRequest, + GetPaymentInstrumentBalanceResponse, + GetPaymentSessionRequest, + GetPaymentSessionResponse, + ListPaymentInstrumentsRequest, + ListPaymentInstrumentsResponse, + ListPaymentSessionsRequest, + ListPaymentSessionsResponse, +} from "@aws-sdk/client-bedrock-agentcore"; +import type { CoreOptions } from "../../core/types"; + +type WithPaymentManagerId = Omit & { managerId: string }; + +export type GetPaymentSessionInput = WithPaymentManagerId; +export type ListPaymentSessionsInput = WithPaymentManagerId; +export type GetPaymentInstrumentInput = WithPaymentManagerId; +export type GetPaymentInstrumentBalanceInput = + WithPaymentManagerId; +export type ListPaymentInstrumentsInput = WithPaymentManagerId; + +export interface CorePaymentClient { + getPaymentManager(id: string, options: CoreOptions): Promise; + listPaymentManagers( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + getPaymentConnector( + managerId: string, + connectorId: string, + options: CoreOptions, + ): Promise; + listPaymentConnectors( + managerId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + + // Core resolves the selected manager ID to the ARN required by the data plane. + getPaymentSession( + request: GetPaymentSessionInput, + options: CoreOptions, + ): Promise; + listPaymentSessions( + request: ListPaymentSessionsInput, + options: CoreOptions, + ): Promise; + getPaymentInstrument( + request: GetPaymentInstrumentInput, + options: CoreOptions, + ): Promise; + getPaymentInstrumentBalance( + request: GetPaymentInstrumentBalanceInput, + options: CoreOptions, + ): Promise; + listPaymentInstruments( + request: ListPaymentInstrumentsInput, + options: CoreOptions, + ): Promise; +} diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index d4246de59..2a128f467 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -17,6 +17,7 @@ describe("createRootHandler", () => { "runtime", "memory", "gateway", + "payment", "eval", "feedback", "config", diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index c41a301cb..5f92616ab 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -4,6 +4,7 @@ import type { CorePolicyClient } from "./gateway/policy/types.tsx"; import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreIdentityClient } from "./identity/types.tsx"; import type { CoreMemoryClient } from "./memory/types.tsx"; +import type { CorePaymentClient } from "./payment/types.tsx"; import type { CoreObservabilityClient, CoreRuntimeClient } from "./runtime/types.tsx"; import type { Context } from "../router"; import type { CoreFetch } from "../core/types"; @@ -19,6 +20,7 @@ export interface Core { eval: CoreEvalClient; observability: CoreObservabilityClient; policy: CorePolicyClient; + payment: CorePaymentClient; projectManager: ProjectManager; /** Imports an alias-pinned Bedrock Agent definition into owned runtime code. */ bedrockAgentImporter: CoreBedrockAgentImporter; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index cf3bdd18e..62909a9ab 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -28,6 +28,11 @@ import type { GetMemoryOutput, ListApiKeyCredentialProvidersResponse, ListOauth2CredentialProvidersResponse, + ListPaymentCredentialProvidersResponse, + GetPaymentConnectorResponse, + GetPaymentManagerResponse, + ListPaymentConnectorsResponse, + ListPaymentManagersResponse, ListAgentRuntimeEndpointsResponse, ListAgentRuntimesResponse, ListAgentRuntimeVersionsResponse, @@ -136,6 +141,14 @@ import type { UpdateApiKeyCredentialProviderInput, UpdateOauth2CredentialProviderInput, } from "../handlers/identity/types"; +import type { + GetPaymentInstrumentResponse, + GetPaymentInstrumentBalanceResponse, + GetPaymentSessionResponse, + ListPaymentInstrumentsResponse, + ListPaymentSessionsResponse, +} from "@aws-sdk/client-bedrock-agentcore"; +import type { CorePaymentClient } from "../handlers/payment/types"; import type { CoreMemoryClient } from "../handlers/memory/types"; import type { CloudWatchLogEvent, @@ -260,6 +273,9 @@ const DEFAULT_CREATE_PAYMENT_RESPONSE = {} as CreatePaymentCredentialProviderRes const DEFAULT_GET_PAYMENT_RESPONSE = {} as GetPaymentCredentialProviderResponse; const DEFAULT_UPDATE_PAYMENT_RESPONSE = {} as UpdatePaymentCredentialProviderResponse; const DEFAULT_DELETE_PAYMENT_RESPONSE = {} as DeletePaymentCredentialProviderResponse; +const DEFAULT_LIST_PAYMENT_PROVIDERS_RESPONSE: ListPaymentCredentialProvidersResponse = { + credentialProviders: [], +}; const DEFAULT_GET_MEMORY_RESPONSE = {} as GetMemoryOutput; const DEFAULT_LIST_MEMORIES_RESPONSE: ListMemoriesOutput = { memories: [] }; const DEFAULT_GET_EVENT_RESPONSE: GetEventOutput = { event: undefined }; @@ -1501,6 +1517,51 @@ export class TestIdentityClient implements CoreIdentityClient { if (this.error) throw this.error; return DEFAULT_DELETE_PAYMENT_RESPONSE; } + + async listPaymentCredentialProviders( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "listPaymentCredentialProviders", + args: [nextToken, maxResults, options], + }); + if (this.error) throw this.error; + return DEFAULT_LIST_PAYMENT_PROVIDERS_RESPONSE; + } +} + +// Payment command tests use real Core clients; configure a stub explicitly if a +// future screen test needs one. +export class TestPaymentClient implements CorePaymentClient { + async getPaymentManager(): Promise { + throw new Error("Unexpected payment call"); + } + async listPaymentManagers(): Promise { + throw new Error("Unexpected payment call"); + } + async getPaymentConnector(): Promise { + throw new Error("Unexpected payment call"); + } + async listPaymentConnectors(): Promise { + throw new Error("Unexpected payment call"); + } + async getPaymentSession(): Promise { + throw new Error("Unexpected payment call"); + } + async listPaymentSessions(): Promise { + throw new Error("Unexpected payment call"); + } + async getPaymentInstrument(): Promise { + throw new Error("Unexpected payment call"); + } + async getPaymentInstrumentBalance(): Promise { + throw new Error("Unexpected payment call"); + } + async listPaymentInstruments(): Promise { + throw new Error("Unexpected payment call"); + } } // TestEvalClient is the eval sub-client of TestCoreClient. @@ -2496,6 +2557,7 @@ export class TestPolicyClient implements CorePolicyClient { export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); readonly identity = new TestIdentityClient(); + readonly payment = new TestPaymentClient(); readonly memory = new TestMemoryClient(); readonly runtime = new TestRuntimeClient(); readonly gateway = new TestGatewayClient();