Skip to content
Draft
92 changes: 86 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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='<paymentManagerId from manager list>'
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='<paymentInstrumentId>'
CONNECTOR_ID='<paymentConnectorId>'
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 '<provider 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
Expand Down
2 changes: 1 addition & 1 deletion src/components/CliOnlyScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
12 changes: 12 additions & 0 deletions src/core/identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GetPaymentCredentialProviderCommand,
ListApiKeyCredentialProvidersCommand,
ListOauth2CredentialProvidersCommand,
ListPaymentCredentialProvidersCommand,
UpdateApiKeyCredentialProviderCommand,
UpdateOauth2CredentialProviderCommand,
UpdatePaymentCredentialProviderCommand,
Expand All @@ -26,6 +27,7 @@ import {
type CreatePaymentCredentialProviderResponse,
type DeletePaymentCredentialProviderResponse,
type GetPaymentCredentialProviderResponse,
type ListPaymentCredentialProvidersResponse,
type UpdatePaymentCredentialProviderResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import type {
Expand Down Expand Up @@ -155,6 +157,16 @@ export class IdentityClient implements CoreIdentityClient {
.send(new GetPaymentCredentialProviderCommand({ name }));
}

async listPaymentCredentialProviders(
nextToken: string | undefined,
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListPaymentCredentialProvidersResponse> {
return this.clients
.control(toClientConfig(options))
.send(new ListPaymentCredentialProvidersCommand({ nextToken, maxResults }));
}

async updatePaymentCredentialProvider(
input: UpdatePaymentCredentialProviderInput,
options: CoreOptions,
Expand Down
3 changes: 3 additions & 0 deletions src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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.
Expand Down
111 changes: 111 additions & 0 deletions src/core/payment.read.test.ts
Original file line number Diff line number Diff line change
@@ -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<AwsClients["control"]>,
);
const data = mock(
(_config: ClientConfig) => ({ send: dataSend }) as unknown as ReturnType<AwsClients["data"]>,
);
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);
});
Loading
Loading